mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-27 04:30:12 +08:00
Update On Mon Sep 9 20:33:15 CEST 2024
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -758,3 +758,4 @@ Update On Thu Sep 5 20:35:23 CEST 2024
|
||||
Update On Fri Sep 6 20:34:58 CEST 2024
|
||||
Update On Sat Sep 7 20:31:47 CEST 2024
|
||||
Update On Sun Sep 8 20:32:53 CEST 2024
|
||||
Update On Mon Sep 9 20:33:04 CEST 2024
|
||||
|
@@ -98,6 +98,10 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
|
||||
return pp.proxies
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Count() int {
|
||||
return len(pp.proxies)
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Touch() {
|
||||
pp.healthCheck.touch()
|
||||
}
|
||||
@@ -126,7 +130,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
|
||||
http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil, pp.Vehicle().Proxy())
|
||||
http.MethodGet, nil, nil, pp.Vehicle().Proxy())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -267,6 +271,10 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
|
||||
return cp.proxies
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Count() int {
|
||||
return len(cp.proxies)
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Touch() {
|
||||
cp.healthCheck.touch()
|
||||
}
|
||||
|
@@ -84,7 +84,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
|
||||
if cfg.addrReuse {
|
||||
addrReuseToListenConfig(lc)
|
||||
}
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CFMA)
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToListenConfig(lc)
|
||||
} else {
|
||||
if cfg.interfaceName != "" {
|
||||
@@ -148,7 +148,7 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
setMultiPathTCP(dialer)
|
||||
}
|
||||
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CFMA)
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToToDialer(dialer)
|
||||
} else {
|
||||
if opt.interfaceName != "" {
|
||||
|
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
// SocketControl
|
||||
// never change type traits because it's used in CFMA
|
||||
// never change type traits because it's used in CMFA
|
||||
type SocketControl func(network, address string, conn syscall.RawConn) error
|
||||
|
||||
// DefaultSocketHook
|
||||
// never change type traits because it's used in CFMA
|
||||
// never change type traits because it's used in CMFA
|
||||
var DefaultSocketHook SocketControl
|
||||
|
||||
func socketHookToToDialer(dialer *net.Dialer) {
|
||||
|
@@ -63,13 +63,13 @@ func TestPool_Basic(t *testing.T) {
|
||||
last := pool.Lookup("bar.com")
|
||||
bar, exist := pool.LookBack(last)
|
||||
|
||||
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5}))
|
||||
assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.Equal(t, pool.Lookup("foo.com"), netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, bar, "bar.com")
|
||||
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1}))
|
||||
assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15}))
|
||||
assert.Equal(t, pool.Gateway(), netip.AddrFrom4([4]byte{192, 168, 0, 1}))
|
||||
assert.Equal(t, pool.Broadcast(), netip.AddrFrom4([4]byte{192, 168, 0, 15}))
|
||||
assert.Equal(t, pool.IPNet().String(), ipnet.String())
|
||||
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5})))
|
||||
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 6})))
|
||||
@@ -91,13 +91,13 @@ func TestPool_BasicV6(t *testing.T) {
|
||||
last := pool.Lookup("bar.com")
|
||||
bar, exist := pool.LookBack(last)
|
||||
|
||||
assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
|
||||
assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
|
||||
assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))
|
||||
assert.Equal(t, first, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
|
||||
assert.Equal(t, pool.Lookup("foo.com"), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
|
||||
assert.Equal(t, last, netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805"))
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, bar, "bar.com")
|
||||
assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
|
||||
assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
|
||||
assert.Equal(t, pool.Gateway(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
|
||||
assert.Equal(t, pool.Broadcast(), netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
|
||||
assert.Equal(t, pool.IPNet().String(), ipnet.String())
|
||||
assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")))
|
||||
assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8806")))
|
||||
@@ -143,8 +143,8 @@ func TestPool_CycleUsed(t *testing.T) {
|
||||
}
|
||||
baz := pool.Lookup("baz.com")
|
||||
next := pool.Lookup("foo.com")
|
||||
assert.True(t, foo == baz)
|
||||
assert.True(t, next == bar)
|
||||
assert.Equal(t, foo, baz)
|
||||
assert.Equal(t, next, bar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestPool_MaxCacheSize(t *testing.T) {
|
||||
pool.Lookup("baz.com")
|
||||
next := pool.Lookup("foo.com")
|
||||
|
||||
assert.False(t, first == next)
|
||||
assert.NotEqual(t, first, next)
|
||||
}
|
||||
|
||||
func TestPool_DoubleMapping(t *testing.T) {
|
||||
@@ -231,7 +231,7 @@ func TestPool_DoubleMapping(t *testing.T) {
|
||||
assert.False(t, bazExist)
|
||||
assert.True(t, barExist)
|
||||
|
||||
assert.False(t, bazIP == newBazIP)
|
||||
assert.NotEqual(t, bazIP, newBazIP)
|
||||
}
|
||||
|
||||
func TestPool_Clone(t *testing.T) {
|
||||
@@ -243,8 +243,8 @@ func TestPool_Clone(t *testing.T) {
|
||||
|
||||
first := pool.Lookup("foo.com")
|
||||
last := pool.Lookup("bar.com")
|
||||
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 5}))
|
||||
assert.Equal(t, first, netip.AddrFrom4([4]byte{192, 168, 0, 4}))
|
||||
assert.Equal(t, last, netip.AddrFrom4([4]byte{192, 168, 0, 5}))
|
||||
|
||||
newPool, _ := New(Options{
|
||||
IPNet: ipnet,
|
||||
@@ -289,13 +289,13 @@ func TestPool_FlushFileCache(t *testing.T) {
|
||||
baz := pool.Lookup("foo.com")
|
||||
nero := pool.Lookup("foo.com")
|
||||
|
||||
assert.True(t, foo == fox)
|
||||
assert.True(t, foo == next)
|
||||
assert.False(t, foo == baz)
|
||||
assert.True(t, bar == bax)
|
||||
assert.True(t, bar == baz)
|
||||
assert.False(t, bar == next)
|
||||
assert.True(t, baz == nero)
|
||||
assert.Equal(t, foo, fox)
|
||||
assert.Equal(t, foo, next)
|
||||
assert.NotEqual(t, foo, baz)
|
||||
assert.Equal(t, bar, bax)
|
||||
assert.Equal(t, bar, baz)
|
||||
assert.NotEqual(t, bar, next)
|
||||
assert.Equal(t, baz, nero)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,11 +318,11 @@ func TestPool_FlushMemoryCache(t *testing.T) {
|
||||
baz := pool.Lookup("foo.com")
|
||||
nero := pool.Lookup("foo.com")
|
||||
|
||||
assert.True(t, foo == fox)
|
||||
assert.True(t, foo == next)
|
||||
assert.False(t, foo == baz)
|
||||
assert.True(t, bar == bax)
|
||||
assert.True(t, bar == baz)
|
||||
assert.False(t, bar == next)
|
||||
assert.True(t, baz == nero)
|
||||
assert.Equal(t, foo, fox)
|
||||
assert.Equal(t, foo, next)
|
||||
assert.NotEqual(t, foo, baz)
|
||||
assert.Equal(t, bar, bax)
|
||||
assert.Equal(t, bar, baz)
|
||||
assert.NotEqual(t, bar, next)
|
||||
assert.Equal(t, baz, nero)
|
||||
}
|
||||
|
@@ -6,8 +6,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/atomic"
|
||||
mihomoHttp "github.com/metacubex/mihomo/component/http"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
@@ -18,12 +20,79 @@ var (
|
||||
initGeoSite bool
|
||||
initGeoIP int
|
||||
initASN bool
|
||||
|
||||
initGeoSiteMutex sync.Mutex
|
||||
initGeoIPMutex sync.Mutex
|
||||
initASNMutex sync.Mutex
|
||||
|
||||
geoIpEnable atomic.Bool
|
||||
geoSiteEnable atomic.Bool
|
||||
asnEnable atomic.Bool
|
||||
|
||||
geoIpUrl string
|
||||
mmdbUrl string
|
||||
geoSiteUrl string
|
||||
asnUrl string
|
||||
)
|
||||
|
||||
func GeoIpUrl() string {
|
||||
return geoIpUrl
|
||||
}
|
||||
|
||||
func SetGeoIpUrl(url string) {
|
||||
geoIpUrl = url
|
||||
}
|
||||
|
||||
func MmdbUrl() string {
|
||||
return mmdbUrl
|
||||
}
|
||||
|
||||
func SetMmdbUrl(url string) {
|
||||
mmdbUrl = url
|
||||
}
|
||||
|
||||
func GeoSiteUrl() string {
|
||||
return geoSiteUrl
|
||||
}
|
||||
|
||||
func SetGeoSiteUrl(url string) {
|
||||
geoSiteUrl = url
|
||||
}
|
||||
|
||||
func ASNUrl() string {
|
||||
return asnUrl
|
||||
}
|
||||
|
||||
func SetASNUrl(url string) {
|
||||
asnUrl = url
|
||||
}
|
||||
|
||||
func downloadToPath(url string, path string) (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func InitGeoSite() error {
|
||||
geoSiteEnable.Store(true)
|
||||
initGeoSiteMutex.Lock()
|
||||
defer initGeoSiteMutex.Unlock()
|
||||
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find GeoSite.dat, start download")
|
||||
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
|
||||
if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
|
||||
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
|
||||
}
|
||||
log.Infoln("Download GeoSite.dat finish")
|
||||
@@ -35,7 +104,7 @@ func InitGeoSite() error {
|
||||
if err := os.Remove(C.Path.GeoSite()); err != nil {
|
||||
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
|
||||
}
|
||||
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
|
||||
if err := downloadToPath(GeoSiteUrl(), C.Path.GeoSite()); err != nil {
|
||||
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -44,49 +113,14 @@ func InitGeoSite() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadGeoSite(path string) (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, C.GeoSiteUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func downloadGeoIP(path string) (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, C.GeoIpUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func InitGeoIP() error {
|
||||
if C.GeodataMode {
|
||||
geoIpEnable.Store(true)
|
||||
initGeoIPMutex.Lock()
|
||||
defer initGeoIPMutex.Unlock()
|
||||
if GeodataMode() {
|
||||
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find GeoIP.dat, start download")
|
||||
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
|
||||
if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
|
||||
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
|
||||
}
|
||||
log.Infoln("Download GeoIP.dat finish")
|
||||
@@ -99,7 +133,7 @@ func InitGeoIP() error {
|
||||
if err := os.Remove(C.Path.GeoIP()); err != nil {
|
||||
return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error())
|
||||
}
|
||||
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
|
||||
if err := downloadToPath(GeoIpUrl(), C.Path.GeoIP()); err != nil {
|
||||
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -110,7 +144,7 @@ func InitGeoIP() error {
|
||||
|
||||
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find MMDB, start download")
|
||||
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil {
|
||||
if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
|
||||
return fmt.Errorf("can't download MMDB: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -121,7 +155,7 @@ func InitGeoIP() error {
|
||||
if err := os.Remove(C.Path.MMDB()); err != nil {
|
||||
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
|
||||
}
|
||||
if err := mmdb.DownloadMMDB(C.Path.MMDB()); err != nil {
|
||||
if err := downloadToPath(MmdbUrl(), C.Path.MMDB()); err != nil {
|
||||
return fmt.Errorf("can't download MMDB: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -131,9 +165,12 @@ func InitGeoIP() error {
|
||||
}
|
||||
|
||||
func InitASN() error {
|
||||
asnEnable.Store(true)
|
||||
initASNMutex.Lock()
|
||||
defer initASNMutex.Unlock()
|
||||
if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find ASN.mmdb, start download")
|
||||
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil {
|
||||
if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
|
||||
return fmt.Errorf("can't download ASN.mmdb: %s", err.Error())
|
||||
}
|
||||
log.Infoln("Download ASN.mmdb finish")
|
||||
@@ -145,7 +182,7 @@ func InitASN() error {
|
||||
if err := os.Remove(C.Path.ASN()); err != nil {
|
||||
return fmt.Errorf("can't remove invalid ASN: %s", err.Error())
|
||||
}
|
||||
if err := mmdb.DownloadASN(C.Path.ASN()); err != nil {
|
||||
if err := downloadToPath(ASNUrl(), C.Path.ASN()); err != nil {
|
||||
return fmt.Errorf("can't download ASN: %s", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -153,3 +190,15 @@ func InitASN() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GeoIpEnable() bool {
|
||||
return geoIpEnable.Load()
|
||||
}
|
||||
|
||||
func GeoSiteEnable() bool {
|
||||
return geoSiteEnable.Load()
|
||||
}
|
||||
|
||||
func ASNEnable() bool {
|
||||
return asnEnable.Load()
|
||||
}
|
||||
|
@@ -13,8 +13,6 @@ import (
|
||||
|
||||
var (
|
||||
geoMode bool
|
||||
AutoUpdate bool
|
||||
UpdateInterval int
|
||||
geoLoaderName = "memconservative"
|
||||
geoSiteMatcher = "succinct"
|
||||
)
|
||||
@@ -25,14 +23,6 @@ func GeodataMode() bool {
|
||||
return geoMode
|
||||
}
|
||||
|
||||
func GeoAutoUpdate() bool {
|
||||
return AutoUpdate
|
||||
}
|
||||
|
||||
func GeoUpdateInterval() int {
|
||||
return UpdateInterval
|
||||
}
|
||||
|
||||
func LoaderName() string {
|
||||
return geoLoaderName
|
||||
}
|
||||
@@ -44,12 +34,6 @@ func SiteMatcherName() string {
|
||||
func SetGeodataMode(newGeodataMode bool) {
|
||||
geoMode = newGeodataMode
|
||||
}
|
||||
func SetGeoAutoUpdate(newAutoUpdate bool) {
|
||||
AutoUpdate = newAutoUpdate
|
||||
}
|
||||
func SetGeoUpdateInterval(newGeoUpdateInterval int) {
|
||||
UpdateInterval = newGeoUpdateInterval
|
||||
}
|
||||
|
||||
func SetLoader(newLoader string) {
|
||||
if newLoader == "memc" {
|
||||
@@ -209,8 +193,11 @@ func LoadGeoIPMatcher(country string) (router.IPMatcher, error) {
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
func ClearCache() {
|
||||
func ClearGeoSiteCache() {
|
||||
loadGeoSiteMatcherListSF.Reset()
|
||||
loadGeoSiteMatcherSF.Reset()
|
||||
}
|
||||
|
||||
func ClearGeoIPCache() {
|
||||
loadGeoIPMatcherSF.Reset()
|
||||
}
|
||||
|
@@ -12,10 +12,21 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/listener/inner"
|
||||
)
|
||||
|
||||
var (
|
||||
ua string
|
||||
)
|
||||
|
||||
func UA() string {
|
||||
return ua
|
||||
}
|
||||
|
||||
func SetUA(UA string) {
|
||||
ua = UA
|
||||
}
|
||||
|
||||
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
|
||||
return HttpRequestWithProxy(ctx, url, method, header, body, "")
|
||||
}
|
||||
@@ -35,7 +46,7 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
|
||||
}
|
||||
|
||||
if _, ok := header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", C.UA)
|
||||
req.Header.Set("User-Agent", UA())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@@ -1,15 +1,9 @@
|
||||
package mmdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
mihomoOnce "github.com/metacubex/mihomo/common/once"
|
||||
mihomoHttp "github.com/metacubex/mihomo/component/http"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
@@ -25,26 +19,26 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
IPreader IPReader
|
||||
ASNreader ASNReader
|
||||
IPonce sync.Once
|
||||
ASNonce sync.Once
|
||||
ipReader IPReader
|
||||
asnReader ASNReader
|
||||
ipOnce sync.Once
|
||||
asnOnce sync.Once
|
||||
)
|
||||
|
||||
func LoadFromBytes(buffer []byte) {
|
||||
IPonce.Do(func() {
|
||||
ipOnce.Do(func() {
|
||||
mmdb, err := maxminddb.FromBytes(buffer)
|
||||
if err != nil {
|
||||
log.Fatalln("Can't load mmdb: %s", err.Error())
|
||||
}
|
||||
IPreader = IPReader{Reader: mmdb}
|
||||
ipReader = IPReader{Reader: mmdb}
|
||||
switch mmdb.Metadata.DatabaseType {
|
||||
case "sing-geoip":
|
||||
IPreader.databaseType = typeSing
|
||||
ipReader.databaseType = typeSing
|
||||
case "Meta-geoip0":
|
||||
IPreader.databaseType = typeMetaV0
|
||||
ipReader.databaseType = typeMetaV0
|
||||
default:
|
||||
IPreader.databaseType = typeMaxmind
|
||||
ipReader.databaseType = typeMaxmind
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -58,83 +52,45 @@ func Verify(path string) bool {
|
||||
}
|
||||
|
||||
func IPInstance() IPReader {
|
||||
IPonce.Do(func() {
|
||||
ipOnce.Do(func() {
|
||||
mmdbPath := C.Path.MMDB()
|
||||
log.Infoln("Load MMDB file: %s", mmdbPath)
|
||||
mmdb, err := maxminddb.Open(mmdbPath)
|
||||
if err != nil {
|
||||
log.Fatalln("Can't load MMDB: %s", err.Error())
|
||||
}
|
||||
IPreader = IPReader{Reader: mmdb}
|
||||
ipReader = IPReader{Reader: mmdb}
|
||||
switch mmdb.Metadata.DatabaseType {
|
||||
case "sing-geoip":
|
||||
IPreader.databaseType = typeSing
|
||||
ipReader.databaseType = typeSing
|
||||
case "Meta-geoip0":
|
||||
IPreader.databaseType = typeMetaV0
|
||||
ipReader.databaseType = typeMetaV0
|
||||
default:
|
||||
IPreader.databaseType = typeMaxmind
|
||||
ipReader.databaseType = typeMaxmind
|
||||
}
|
||||
})
|
||||
|
||||
return IPreader
|
||||
}
|
||||
|
||||
func DownloadMMDB(path string) (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, C.MmdbUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
|
||||
return err
|
||||
return ipReader
|
||||
}
|
||||
|
||||
func ASNInstance() ASNReader {
|
||||
ASNonce.Do(func() {
|
||||
asnOnce.Do(func() {
|
||||
ASNPath := C.Path.ASN()
|
||||
log.Infoln("Load ASN file: %s", ASNPath)
|
||||
asn, err := maxminddb.Open(ASNPath)
|
||||
if err != nil {
|
||||
log.Fatalln("Can't load ASN: %s", err.Error())
|
||||
}
|
||||
ASNreader = ASNReader{Reader: asn}
|
||||
asnReader = ASNReader{Reader: asn}
|
||||
})
|
||||
|
||||
return ASNreader
|
||||
}
|
||||
|
||||
func DownloadASN(path string) (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
|
||||
return err
|
||||
return asnReader
|
||||
}
|
||||
|
||||
func ReloadIP() {
|
||||
mihomoOnce.Reset(&IPonce)
|
||||
mihomoOnce.Reset(&ipOnce)
|
||||
}
|
||||
|
||||
func ReloadASN() {
|
||||
mihomoOnce.Reset(&ASNonce)
|
||||
mihomoOnce.Reset(&asnOnce)
|
||||
}
|
||||
|
@@ -8,11 +8,11 @@ func InstallOverride(override *maxminddb.Reader) {
|
||||
newReader := IPReader{Reader: override}
|
||||
switch override.Metadata.DatabaseType {
|
||||
case "sing-geoip":
|
||||
IPreader.databaseType = typeSing
|
||||
ipReader.databaseType = typeSing
|
||||
case "Meta-geoip0":
|
||||
IPreader.databaseType = typeMetaV0
|
||||
ipReader.databaseType = typeMetaV0
|
||||
default:
|
||||
IPreader.databaseType = typeMaxmind
|
||||
ipReader.databaseType = typeMaxmind
|
||||
}
|
||||
IPreader = newReader
|
||||
ipReader = newReader
|
||||
}
|
||||
|
@@ -23,11 +23,11 @@ func FindProcessName(network string, srcIP netip.Addr, srcPort int) (uint32, str
|
||||
}
|
||||
|
||||
// PackageNameResolver
|
||||
// never change type traits because it's used in CFMA
|
||||
// never change type traits because it's used in CMFA
|
||||
type PackageNameResolver func(metadata *C.Metadata) (string, error)
|
||||
|
||||
// DefaultPackageNameResolver
|
||||
// never change type traits because it's used in CFMA
|
||||
// never change type traits because it's used in CMFA
|
||||
var DefaultPackageNameResolver PackageNameResolver
|
||||
|
||||
func FindPackageName(metadata *C.Metadata) (string, error) {
|
||||
|
@@ -237,7 +237,7 @@ const MaxPackageFileSize = 32 * 1024 * 1024
|
||||
func downloadPackageFile() (err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http request failed: %w", err)
|
||||
}
|
||||
@@ -418,7 +418,7 @@ func copyFile(src, dst string) error {
|
||||
func getLatestVersion() (version string, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get Latest Version fail: %w", err)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/atomic"
|
||||
"github.com/metacubex/mihomo/common/batch"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
_ "github.com/metacubex/mihomo/component/geodata/standard"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
@@ -18,69 +20,87 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
UpdatingGeo atomic.Bool
|
||||
autoUpdate bool
|
||||
updateInterval int
|
||||
|
||||
updatingGeo atomic.Bool
|
||||
)
|
||||
|
||||
func updateGeoDatabases() error {
|
||||
defer runtime.GC()
|
||||
geoLoader, err := geodata.GetGeoDataLoader("standard")
|
||||
func GeoAutoUpdate() bool {
|
||||
return autoUpdate
|
||||
}
|
||||
|
||||
func GeoUpdateInterval() int {
|
||||
return updateInterval
|
||||
}
|
||||
|
||||
func SetGeoAutoUpdate(newAutoUpdate bool) {
|
||||
autoUpdate = newAutoUpdate
|
||||
}
|
||||
|
||||
func SetGeoUpdateInterval(newGeoUpdateInterval int) {
|
||||
updateInterval = newGeoUpdateInterval
|
||||
}
|
||||
|
||||
func UpdateMMDB() (err error) {
|
||||
defer mmdb.ReloadIP()
|
||||
data, err := downloadForBytes(geodata.MmdbUrl())
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("can't download MMDB database file: %w", err)
|
||||
}
|
||||
instance, err := maxminddb.FromBytes(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid MMDB database file: %s", err)
|
||||
}
|
||||
_ = instance.Close()
|
||||
|
||||
mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
|
||||
if err = saveFile(data, C.Path.MMDB()); err != nil {
|
||||
return fmt.Errorf("can't save MMDB database file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateASN() (err error) {
|
||||
defer mmdb.ReloadASN()
|
||||
data, err := downloadForBytes(geodata.ASNUrl())
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download ASN database file: %w", err)
|
||||
}
|
||||
|
||||
if C.GeodataMode {
|
||||
data, err := downloadForBytes(C.GeoIpUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download GeoIP database file: %w", err)
|
||||
}
|
||||
|
||||
if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil {
|
||||
return fmt.Errorf("invalid GeoIP database file: %s", err)
|
||||
}
|
||||
|
||||
if err = saveFile(data, C.Path.GeoIP()); err != nil {
|
||||
return fmt.Errorf("can't save GeoIP database file: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
defer mmdb.ReloadIP()
|
||||
data, err := downloadForBytes(C.MmdbUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download MMDB database file: %w", err)
|
||||
}
|
||||
|
||||
instance, err := maxminddb.FromBytes(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid MMDB database file: %s", err)
|
||||
}
|
||||
_ = instance.Close()
|
||||
|
||||
mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
|
||||
if err = saveFile(data, C.Path.MMDB()); err != nil {
|
||||
return fmt.Errorf("can't save MMDB database file: %w", err)
|
||||
}
|
||||
instance, err := maxminddb.FromBytes(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid ASN database file: %s", err)
|
||||
}
|
||||
_ = instance.Close()
|
||||
|
||||
if C.ASNEnable {
|
||||
defer mmdb.ReloadASN()
|
||||
data, err := downloadForBytes(C.ASNUrl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download ASN database file: %w", err)
|
||||
}
|
||||
|
||||
instance, err := maxminddb.FromBytes(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid ASN database file: %s", err)
|
||||
}
|
||||
_ = instance.Close()
|
||||
|
||||
mmdb.ASNInstance().Reader.Close()
|
||||
if err = saveFile(data, C.Path.ASN()); err != nil {
|
||||
return fmt.Errorf("can't save ASN database file: %w", err)
|
||||
}
|
||||
mmdb.ASNInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file
|
||||
if err = saveFile(data, C.Path.ASN()); err != nil {
|
||||
return fmt.Errorf("can't save ASN database file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := downloadForBytes(C.GeoSiteUrl)
|
||||
func UpdateGeoIp() (err error) {
|
||||
defer geodata.ClearGeoIPCache()
|
||||
geoLoader, err := geodata.GetGeoDataLoader("standard")
|
||||
data, err := downloadForBytes(geodata.GeoIpUrl())
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download GeoIP database file: %w", err)
|
||||
}
|
||||
if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil {
|
||||
return fmt.Errorf("invalid GeoIP database file: %s", err)
|
||||
}
|
||||
if err = saveFile(data, C.Path.GeoIP()); err != nil {
|
||||
return fmt.Errorf("can't save GeoIP database file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateGeoSite() (err error) {
|
||||
defer geodata.ClearGeoSiteCache()
|
||||
geoLoader, err := geodata.GetGeoDataLoader("standard")
|
||||
data, err := downloadForBytes(geodata.GeoSiteUrl())
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't download GeoSite database file: %w", err)
|
||||
}
|
||||
@@ -92,8 +112,45 @@ func updateGeoDatabases() error {
|
||||
if err = saveFile(data, C.Path.GeoSite()); err != nil {
|
||||
return fmt.Errorf("can't save GeoSite database file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
geodata.ClearCache()
|
||||
func updateGeoDatabases() error {
|
||||
defer runtime.GC()
|
||||
|
||||
b, _ := batch.New[interface{}](context.Background())
|
||||
|
||||
if geodata.GeoIpEnable() {
|
||||
if geodata.GeodataMode() {
|
||||
b.Go("UpdateGeoIp", func() (_ interface{}, err error) {
|
||||
err = UpdateGeoIp()
|
||||
return
|
||||
})
|
||||
} else {
|
||||
b.Go("UpdateMMDB", func() (_ interface{}, err error) {
|
||||
err = UpdateMMDB()
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if geodata.ASNEnable() {
|
||||
b.Go("UpdateASN", func() (_ interface{}, err error) {
|
||||
err = UpdateASN()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
if geodata.GeoSiteEnable() {
|
||||
b.Go("UpdateGeoSite", func() (_ interface{}, err error) {
|
||||
err = UpdateGeoSite()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
if e := b.Wait(); e != nil {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -103,12 +160,12 @@ var ErrGetDatabaseUpdateSkip = errors.New("GEO database is updating, skip")
|
||||
func UpdateGeoDatabases() error {
|
||||
log.Infoln("[GEO] Start updating GEO database")
|
||||
|
||||
if UpdatingGeo.Load() {
|
||||
if updatingGeo.Load() {
|
||||
return ErrGetDatabaseUpdateSkip
|
||||
}
|
||||
|
||||
UpdatingGeo.Store(true)
|
||||
defer UpdatingGeo.Store(false)
|
||||
updatingGeo.Store(true)
|
||||
defer updatingGeo.Store(false)
|
||||
|
||||
log.Infoln("[GEO] Updating GEO database")
|
||||
|
||||
@@ -122,7 +179,7 @@ func UpdateGeoDatabases() error {
|
||||
|
||||
func getUpdateTime() (err error, time time.Time) {
|
||||
var fileInfo os.FileInfo
|
||||
if C.GeodataMode {
|
||||
if geodata.GeodataMode() {
|
||||
fileInfo, err = os.Stat(C.Path.GeoIP())
|
||||
if err != nil {
|
||||
return err, time
|
||||
@@ -138,13 +195,13 @@ func getUpdateTime() (err error, time time.Time) {
|
||||
}
|
||||
|
||||
func RegisterGeoUpdater() {
|
||||
if C.GeoUpdateInterval <= 0 {
|
||||
log.Errorln("[GEO] Invalid update interval: %d", C.GeoUpdateInterval)
|
||||
if updateInterval <= 0 {
|
||||
log.Errorln("[GEO] Invalid update interval: %d", updateInterval)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour)
|
||||
ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
err, lastUpdate := getUpdateTime()
|
||||
@@ -154,8 +211,8 @@ func RegisterGeoUpdater() {
|
||||
}
|
||||
|
||||
log.Infoln("[GEO] last update time %s", lastUpdate)
|
||||
if lastUpdate.Add(time.Duration(C.GeoUpdateInterval) * time.Hour).Before(time.Now()) {
|
||||
log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(C.GeoUpdateInterval)*time.Hour)
|
||||
if lastUpdate.Add(time.Duration(updateInterval) * time.Hour).Before(time.Now()) {
|
||||
log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(updateInterval)*time.Hour)
|
||||
if err := UpdateGeoDatabases(); err != nil {
|
||||
log.Errorln("[GEO] Failed to update GEO database: %s", err.Error())
|
||||
return
|
||||
@@ -163,7 +220,7 @@ func RegisterGeoUpdater() {
|
||||
}
|
||||
|
||||
for range ticker.C {
|
||||
log.Infoln("[GEO] updating database every %d hours", C.GeoUpdateInterval)
|
||||
log.Infoln("[GEO] updating database every %d hours", updateInterval)
|
||||
if err := UpdateGeoDatabases(); err != nil {
|
||||
log.Errorln("[GEO] Failed to update GEO database: %s", err.Error())
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
mihomoHttp "github.com/metacubex/mihomo/component/http"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
@@ -17,7 +16,7 @@ import (
|
||||
func downloadForBytes(url string) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
|
||||
defer cancel()
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
|
||||
resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/metacubex/mihomo/component/cidr"
|
||||
"github.com/metacubex/mihomo/component/fakeip"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
mihomoHttp "github.com/metacubex/mihomo/component/http"
|
||||
P "github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/component/sniffer"
|
||||
@@ -433,7 +434,7 @@ func DefaultRawConfig() *RawConfig {
|
||||
Mode: T.Rule,
|
||||
GeoAutoUpdate: false,
|
||||
GeoUpdateInterval: 24,
|
||||
GeodataMode: C.GeodataMode,
|
||||
GeodataMode: geodata.GeodataMode(),
|
||||
GeodataLoader: "memconservative",
|
||||
UnifiedDelay: false,
|
||||
Authentication: []string{},
|
||||
@@ -681,19 +682,16 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
||||
}
|
||||
|
||||
func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||
updater.SetGeoAutoUpdate(cfg.GeoAutoUpdate)
|
||||
updater.SetGeoUpdateInterval(cfg.GeoUpdateInterval)
|
||||
geodata.SetGeodataMode(cfg.GeodataMode)
|
||||
geodata.SetGeoAutoUpdate(cfg.GeoAutoUpdate)
|
||||
geodata.SetGeoUpdateInterval(cfg.GeoUpdateInterval)
|
||||
geodata.SetLoader(cfg.GeodataLoader)
|
||||
geodata.SetSiteMatcher(cfg.GeositeMatcher)
|
||||
C.GeoAutoUpdate = cfg.GeoAutoUpdate
|
||||
C.GeoUpdateInterval = cfg.GeoUpdateInterval
|
||||
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
|
||||
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
|
||||
C.MmdbUrl = cfg.GeoXUrl.Mmdb
|
||||
C.ASNUrl = cfg.GeoXUrl.ASN
|
||||
C.GeodataMode = cfg.GeodataMode
|
||||
C.UA = cfg.GlobalUA
|
||||
geodata.SetGeoIpUrl(cfg.GeoXUrl.GeoIp)
|
||||
geodata.SetGeoSiteUrl(cfg.GeoXUrl.GeoSite)
|
||||
geodata.SetMmdbUrl(cfg.GeoXUrl.Mmdb)
|
||||
geodata.SetASNUrl(cfg.GeoXUrl.ASN)
|
||||
mihomoHttp.SetUA(cfg.GlobalUA)
|
||||
|
||||
if cfg.KeepAliveIdle != 0 {
|
||||
N.KeepAliveIdle = time.Duration(cfg.KeepAliveIdle) * time.Second
|
||||
|
@@ -1,12 +0,0 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
ASNEnable bool
|
||||
GeodataMode bool
|
||||
GeoAutoUpdate bool
|
||||
GeoUpdateInterval int
|
||||
GeoIpUrl string
|
||||
MmdbUrl string
|
||||
GeoSiteUrl string
|
||||
ASNUrl string
|
||||
)
|
@@ -1,5 +0,0 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
UA string
|
||||
)
|
@@ -71,6 +71,7 @@ type Provider interface {
|
||||
type ProxyProvider interface {
|
||||
Provider
|
||||
Proxies() []constant.Proxy
|
||||
Count() int
|
||||
// Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
||||
// Commonly used in DialContext and DialPacketConn
|
||||
Touch()
|
||||
|
@@ -955,12 +955,12 @@ rule-providers:
|
||||
# 对于behavior=domain:
|
||||
# - format=yaml 可以通过“mihomo convert-ruleset domain yaml XXX.yaml XXX.mrs”转换到mrs格式
|
||||
# - format=text 可以通过“mihomo convert-ruleset domain text XXX.text XXX.mrs”转换到mrs格式
|
||||
# - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回ymal格式)
|
||||
# - XXX.mrs 可以通过"mihomo convert-ruleset domain mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式)
|
||||
#
|
||||
# 对于behavior=ipcidr:
|
||||
# - format=yaml 可以通过“mihomo convert-ruleset ipcidr yaml XXX.yaml XXX.mrs”转换到mrs格式
|
||||
# - format=text 可以通过“mihomo convert-ruleset ipcidr text XXX.text XXX.mrs”转换到mrs格式
|
||||
# - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回ymal格式)
|
||||
# - XXX.mrs 可以通过"mihomo convert-ruleset ipcidr mrs XXX.mrs XXX.text"转换回text格式(暂不支持转换回yaml格式)
|
||||
#
|
||||
type: http
|
||||
url: "url"
|
||||
|
@@ -20,7 +20,7 @@ require (
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
|
||||
github.com/metacubex/chacha v0.1.0
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4
|
||||
github.com/metacubex/randv2 v0.2.0
|
||||
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8
|
||||
|
@@ -104,8 +104,8 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58 h1:T6OxROLZBr9SOQxN5TzUslv81hEREy/dEgaUKVjaG7U=
|
||||
github.com/metacubex/quic-go v0.46.1-0.20240807232329-1c6cb2d67f58/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o=
|
||||
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA=
|
||||
|
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
G "github.com/metacubex/mihomo/component/geodata"
|
||||
mihomoHttp "github.com/metacubex/mihomo/component/http"
|
||||
"github.com/metacubex/mihomo/component/iface"
|
||||
"github.com/metacubex/mihomo/component/profile"
|
||||
"github.com/metacubex/mihomo/component/profile/cachefile"
|
||||
@@ -81,6 +82,7 @@ func ParseWithBytes(buf []byte) (*config.Config, error) {
|
||||
func ApplyConfig(cfg *config.Config, force bool) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
log.SetLevel(cfg.General.LogLevel)
|
||||
|
||||
tunnel.OnSuspend()
|
||||
|
||||
@@ -115,8 +117,6 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
||||
tunnel.OnRunning()
|
||||
hcCompatibleProvider(cfg.Providers)
|
||||
initExternalUI()
|
||||
|
||||
log.SetLevel(cfg.General.LogLevel)
|
||||
}
|
||||
|
||||
func initInnerTcp() {
|
||||
@@ -157,13 +157,13 @@ func GetGeneral() *config.General {
|
||||
Interface: dialer.DefaultInterface.Load(),
|
||||
RoutingMark: int(dialer.DefaultRoutingMark.Load()),
|
||||
GeoXUrl: config.GeoXUrl{
|
||||
GeoIp: C.GeoIpUrl,
|
||||
Mmdb: C.MmdbUrl,
|
||||
ASN: C.ASNUrl,
|
||||
GeoSite: C.GeoSiteUrl,
|
||||
GeoIp: G.GeoIpUrl(),
|
||||
Mmdb: G.MmdbUrl(),
|
||||
ASN: G.ASNUrl(),
|
||||
GeoSite: G.GeoSiteUrl(),
|
||||
},
|
||||
GeoAutoUpdate: G.GeoAutoUpdate(),
|
||||
GeoUpdateInterval: G.GeoUpdateInterval(),
|
||||
GeoAutoUpdate: updater.GeoAutoUpdate(),
|
||||
GeoUpdateInterval: updater.GeoUpdateInterval(),
|
||||
GeodataMode: G.GeodataMode(),
|
||||
GeodataLoader: G.LoaderName(),
|
||||
GeositeMatcher: G.SiteMatcherName(),
|
||||
@@ -171,7 +171,7 @@ func GetGeneral() *config.General {
|
||||
FindProcessMode: tunnel.FindProcessMode(),
|
||||
Sniffing: tunnel.IsSniffing(),
|
||||
GlobalClientFingerprint: tlsC.GetGlobalFingerprint(),
|
||||
GlobalUA: C.UA,
|
||||
GlobalUA: mihomoHttp.UA(),
|
||||
}
|
||||
|
||||
return general
|
||||
|
@@ -37,7 +37,7 @@ func New(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener
|
||||
}
|
||||
|
||||
// NewWithAuthenticate
|
||||
// never change type traits because it's used in CFMA
|
||||
// never change type traits because it's used in CMFA
|
||||
func NewWithAuthenticate(addr string, tunnel C.Tunnel, authenticate bool, additions ...inbound.Addition) (*Listener, error) {
|
||||
getAuth := authStore.Authenticator
|
||||
if !authenticate {
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
@@ -78,7 +79,7 @@ func main() {
|
||||
}
|
||||
|
||||
if geodataMode {
|
||||
C.GeodataMode = true
|
||||
geodata.SetGeodataMode(true)
|
||||
}
|
||||
|
||||
if configString != "" {
|
||||
@@ -140,7 +141,7 @@ func main() {
|
||||
log.Fatalln("Parse config error: %s", err.Error())
|
||||
}
|
||||
|
||||
if C.GeoAutoUpdate {
|
||||
if updater.GeoAutoUpdate() {
|
||||
updater.RegisterGeoUpdater()
|
||||
}
|
||||
|
||||
|
@@ -46,7 +46,7 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) {
|
||||
return g.isLan(ip), g.adapter
|
||||
}
|
||||
|
||||
if C.GeodataMode {
|
||||
if geodata.GeodataMode() {
|
||||
if g.isSourceIP {
|
||||
if slices.Contains(metadata.SrcGeoIP, g.country) {
|
||||
return true, g.adapter
|
||||
@@ -102,7 +102,7 @@ func (g *GEOIP) MatchIp(ip netip.Addr) bool {
|
||||
return g.isLan(ip)
|
||||
}
|
||||
|
||||
if C.GeodataMode {
|
||||
if geodata.GeodataMode() {
|
||||
matcher, err := g.getIPMatcher()
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -124,7 +124,7 @@ func (g dnsFallbackFilter) MatchIp(ip netip.Addr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if C.GeodataMode {
|
||||
if geodata.GeodataMode() {
|
||||
matcher, err := g.getIPMatcher()
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -170,7 +170,7 @@ func (g *GEOIP) GetCountry() string {
|
||||
}
|
||||
|
||||
func (g *GEOIP) GetIPMatcher() (router.IPMatcher, error) {
|
||||
if C.GeodataMode {
|
||||
if geodata.GeodataMode() {
|
||||
return g.getIPMatcher()
|
||||
}
|
||||
return nil, errors.New("not geodata mode")
|
||||
@@ -193,10 +193,6 @@ func (g *GEOIP) GetRecodeSize() int {
|
||||
}
|
||||
|
||||
func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP, error) {
|
||||
if err := geodata.InitGeoIP(); err != nil {
|
||||
log.Errorln("can't initial GeoIP: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
country = strings.ToLower(country)
|
||||
|
||||
geoip := &GEOIP{
|
||||
@@ -206,11 +202,17 @@ func NewGEOIP(country string, adapter string, isSrc, noResolveIP bool) (*GEOIP,
|
||||
noResolveIP: noResolveIP,
|
||||
isSourceIP: isSrc,
|
||||
}
|
||||
if !C.GeodataMode || country == "lan" {
|
||||
|
||||
if country == "lan" {
|
||||
return geoip, nil
|
||||
}
|
||||
|
||||
if C.GeodataMode {
|
||||
if err := geodata.InitGeoIP(); err != nil {
|
||||
log.Errorln("can't initial GeoIP: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if geodata.GeodataMode() {
|
||||
geoIPMatcher, err := geoip.getIPMatcher() // test load
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@@ -63,7 +63,6 @@ func (a *ASN) GetASN() string {
|
||||
}
|
||||
|
||||
func NewIPASN(asn string, adapter string, isSrc, noResolveIP bool) (*ASN, error) {
|
||||
C.ASNEnable = true
|
||||
if err := geodata.InitASN(); err != nil {
|
||||
log.Errorln("can't initial ASN: %s", err)
|
||||
return nil, err
|
||||
|
4
clash-nyanpasu/.github/workflows/ci.yml
vendored
4
clash-nyanpasu/.github/workflows/ci.yml
vendored
@@ -35,7 +35,9 @@ jobs:
|
||||
sudo apt-get install -y
|
||||
libgtk-3-dev
|
||||
libayatana-appindicator3-dev
|
||||
libwebkit2gtk-4.0-dev
|
||||
libwebkit2gtk-4.1-dev
|
||||
librsvg2-dev
|
||||
libxdo-dev
|
||||
webkit2gtk-driver
|
||||
xvfb
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Setup Toolchain
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./backend/"
|
||||
@@ -97,8 +97,8 @@ jobs:
|
||||
if: ${{ inputs.aarch64 == false }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
with:
|
||||
tagName: ${{ inputs.tag }}
|
||||
@@ -113,8 +113,8 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
||||
|
@@ -107,8 +107,8 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
with:
|
||||
@@ -125,8 +125,8 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
run: |
|
||||
|
@@ -85,8 +85,8 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
with:
|
||||
tagName: ${{ inputs.tag }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: ${{ inputs.tag }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
NIGHTLY: ${{ inputs.nightly == true && 'true' || 'false' }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
|
@@ -50,8 +50,8 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: dev
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
pnpm build --target aarch64-apple-darwin
|
||||
pnpm upload:osx-aarch64
|
||||
|
@@ -36,15 +36,15 @@ jobs:
|
||||
tag: "pre-release"
|
||||
secrets: inherit
|
||||
|
||||
linux_aarch64_build:
|
||||
name: Linux aarch64 Build
|
||||
uses: ./.github/workflows/deps-build-linux.yaml
|
||||
needs: [delete_current_releases]
|
||||
with:
|
||||
nightly: true
|
||||
tag: "pre-release"
|
||||
aarch64: true
|
||||
secrets: inherit
|
||||
# linux_aarch64_build:
|
||||
# name: Linux aarch64 Build
|
||||
# uses: ./.github/workflows/deps-build-linux.yaml
|
||||
# needs: [delete_current_releases]
|
||||
# with:
|
||||
# nightly: true
|
||||
# tag: "pre-release"
|
||||
# aarch64: true
|
||||
# secrets: inherit
|
||||
|
||||
macos_amd64_build:
|
||||
name: macOS amd64 Build
|
||||
@@ -68,11 +68,10 @@ jobs:
|
||||
|
||||
update_tag:
|
||||
name: Update tag
|
||||
needs:
|
||||
[
|
||||
needs: [
|
||||
windows_build,
|
||||
linux_amd64_build,
|
||||
linux_aarch64_build,
|
||||
# linux_aarch64_build,
|
||||
macos_amd64_build,
|
||||
macos_aarch64_build,
|
||||
]
|
||||
|
@@ -8,3 +8,4 @@ pnpm-lock.yaml
|
||||
*.wxs
|
||||
frontend/nyanpasu/src/router.ts
|
||||
frontend/nyanpasu/auto-imports.d.ts
|
||||
backend/tauri/gen/schemas/
|
||||
|
1862
clash-nyanpasu/backend/Cargo.lock
generated
1862
clash-nyanpasu/backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["tauri", "webview2-com-bridge", "boa_utils"]
|
||||
members = ["tauri", "boa_utils"]
|
||||
|
||||
[workspace.package]
|
||||
repository = "https://github.com/keiko233/clash-nyanpasu.git"
|
||||
@@ -11,7 +11,7 @@ authors = ["zzzgydi", "keiko233"]
|
||||
[workspace.dependencies]
|
||||
thiserror = "1"
|
||||
tracing = "0.1"
|
||||
boa_engine = "0.19"
|
||||
boa_engine = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
|
||||
|
||||
[profile.release]
|
||||
panic = "unwind"
|
||||
|
@@ -3,8 +3,8 @@ image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge"
|
||||
pre-build = [
|
||||
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||
"""apt-get update && apt-get -y install \
|
||||
libwebkit2gtk-4.0-dev:$CROSS_DEB_ARCH \
|
||||
libgtk-3-dev:$CROSS_DEB_ARCH \
|
||||
libwebkit2gtk-4.1-dev:$CROSS_DEB_ARCH \
|
||||
libxdo-dev:$CROSS_DEB_ARCH \
|
||||
libayatana-appindicator3-dev:$CROSS_DEB_ARCH \
|
||||
librsvg2-dev:$CROSS_DEB_ARCH \
|
||||
libpango1.0-dev:$CROSS_DEB_ARCH \
|
||||
|
@@ -9,8 +9,8 @@ authors.workspace = true
|
||||
[dependencies]
|
||||
rustc-hash = { version = "2", features = ["std"] }
|
||||
boa_engine.workspace = true
|
||||
boa_gc = "0.19"
|
||||
boa_parser = "0.19"
|
||||
boa_gc = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
|
||||
boa_parser = { version = "0.19", git = "https://github.com/boa-dev/boa.git", branch = "backport-0.19-fixes" }
|
||||
isahc = "1.7"
|
||||
futures-util = "0.3"
|
||||
smol = "2"
|
||||
|
@@ -9,8 +9,12 @@ default-run = "clash-nyanpasu"
|
||||
edition = { workspace = true }
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1", features = [] }
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
serde = "1"
|
||||
simd-json = "0.13"
|
||||
chrono = "0.4"
|
||||
@@ -46,26 +50,10 @@ tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
|
||||
relative-path = "1.9"
|
||||
tauri = { version = "1.5.4", features = [
|
||||
"dialog-all",
|
||||
"updater",
|
||||
"fs-all",
|
||||
"clipboard-all",
|
||||
"os-all",
|
||||
"global-shortcut-all",
|
||||
"notification-all",
|
||||
"process-all",
|
||||
"shell-all",
|
||||
"system-tray",
|
||||
"window-all",
|
||||
] }
|
||||
window-vibrancy = { version = "0.5.0" }
|
||||
window-shadows = { version = "0.2.2" }
|
||||
axum = "0.7"
|
||||
mime = "0.3"
|
||||
bincode = "1"
|
||||
bytes = { version = "1", features = ["serde"] }
|
||||
wry = { version = "0.24.6" }
|
||||
semver = "1.0"
|
||||
zip = "2.0.0"
|
||||
zip-extensions = "0.8.0"
|
||||
@@ -81,7 +69,11 @@ runas = { git = "https://github.com/libnyanpasu/rust-runas.git" }
|
||||
backon = { version = "1.0.1", features = ["tokio-sleep"] }
|
||||
rust-i18n = "3"
|
||||
adler = "1.0.2"
|
||||
rfd = "0.10" # should bump to v0.14 when clarify why the rfd v0.10 from tauri breaks build
|
||||
rfd = { version = "0.14", default-features = false, features = [
|
||||
"tokio",
|
||||
"gtk3",
|
||||
"common-controls-v6",
|
||||
] }
|
||||
indexmap = { version = "2.2.3", features = ["serde"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-attributes = "0.1"
|
||||
@@ -96,7 +88,6 @@ tracing-log = { version = "0.2" }
|
||||
tracing-appender = { version = "0.2", features = ["parking_lot"] }
|
||||
base64 = "0.22"
|
||||
single-instance = "0.3.3"
|
||||
tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" }
|
||||
uuid = "1.7.0"
|
||||
image = "0.25.0"
|
||||
fast_image_resize = "4"
|
||||
@@ -138,12 +129,33 @@ mlua = { version = "0.9", features = [
|
||||
] }
|
||||
enumflags2 = "0.7"
|
||||
sha2 = "0.10"
|
||||
bimap = "0.6.3"
|
||||
|
||||
# Tauri Dependencies
|
||||
tauri = { version = "2.0.0-rc", features = [
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
"image-ico",
|
||||
] }
|
||||
tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" }
|
||||
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
window-vibrancy = { version = "0.5.2" }
|
||||
|
||||
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
|
||||
tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
||||
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.25.0"
|
||||
cocoa = "0.26.0"
|
||||
objc = "0.2.7"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
@@ -153,34 +165,8 @@ windows-sys = { version = "0.59", features = [
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_SystemInformation",
|
||||
] }
|
||||
webview2-com-bridge = { path = "../webview2-com-bridge" }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.tauri]
|
||||
version = "1.5.4"
|
||||
features = [
|
||||
"global-shortcut-all",
|
||||
"icon-png",
|
||||
"process-all",
|
||||
"dialog-all",
|
||||
"shell-all",
|
||||
"system-tray",
|
||||
"updater",
|
||||
"window-all",
|
||||
]
|
||||
|
||||
[target.'cfg(linux)'.dependencies.tauri]
|
||||
version = "1.5.4"
|
||||
features = [
|
||||
"global-shortcut-all",
|
||||
"process-all",
|
||||
"dialog-all",
|
||||
"shell-all",
|
||||
"system-tray",
|
||||
"updater",
|
||||
"window-all",
|
||||
"native-tls-vendored",
|
||||
"reqwest-native-tls-vendored",
|
||||
]
|
||||
windows-core = "0.58.0"
|
||||
webview2-com = "0.33"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol", "default-meta"]
|
||||
|
5
clash-nyanpasu/backend/tauri/capabilities/main.json
Normal file
5
clash-nyanpasu/backend/tauri/capabilities/main.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main-capability",
|
||||
"permissions": ["updater:default"]
|
||||
}
|
76
clash-nyanpasu/backend/tauri/capabilities/migrated.json
Normal file
76
clash-nyanpasu/backend/tauri/capabilities/migrated.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-copy-file",
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-rename",
|
||||
"fs:allow-exists",
|
||||
"core:window:allow-create",
|
||||
"core:window:allow-center",
|
||||
"core:window:allow-request-user-attention",
|
||||
"core:window:allow-set-resizable",
|
||||
"core:window:allow-set-maximizable",
|
||||
"core:window:allow-set-minimizable",
|
||||
"core:window:allow-set-closable",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-unmaximize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-set-decorations",
|
||||
"core:window:allow-set-always-on-top",
|
||||
"core:window:allow-set-content-protected",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-set-min-size",
|
||||
"core:window:allow-set-max-size",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-icon",
|
||||
"core:window:allow-set-skip-taskbar",
|
||||
"core:window:allow-set-cursor-grab",
|
||||
"core:window:allow-set-cursor-visible",
|
||||
"core:window:allow-set-cursor-icon",
|
||||
"core:window:allow-set-cursor-position",
|
||||
"core:window:allow-set-ignore-cursor-events",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:webview:allow-print",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"dialog:allow-message",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-confirm",
|
||||
"notification:default",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-register-all",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-unregister-all",
|
||||
"os:allow-platform",
|
||||
"os:allow-version",
|
||||
"os:allow-os-type",
|
||||
"os:allow-family",
|
||||
"os:allow-arch",
|
||||
"os:allow-exe-extension",
|
||||
"os:allow-locale",
|
||||
"os:allow-hostname",
|
||||
"process:allow-restart",
|
||||
"process:allow-exit",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text"
|
||||
]
|
||||
}
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","notification:default","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
7244
clash-nyanpasu/backend/tauri/gen/schemas/desktop-schema.json
Normal file
7244
clash-nyanpasu/backend/tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
7244
clash-nyanpasu/backend/tauri/gen/schemas/linux-schema.json
Normal file
7244
clash-nyanpasu/backend/tauri/gen/schemas/linux-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
7244
clash-nyanpasu/backend/tauri/gen/schemas/windows-schema.json
Normal file
7244
clash-nyanpasu/backend/tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/1.x/tooling/cli/schema.json",
|
||||
"package": {
|
||||
"version": "1.6.2"
|
||||
},
|
||||
"tauri": {
|
||||
"$schema": "../../../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"version": "2.0.0",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
|
||||
"endpoints": [
|
||||
"https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-nightly-proxy.json",
|
||||
|
@@ -31,6 +31,7 @@ pub static BUILD_INFO: Lazy<BuildInfo> = Lazy::new(|| BuildInfo {
|
||||
|
||||
pub static IS_APPIMAGE: Lazy<bool> = Lazy::new(|| std::env::var("APPIMAGE").is_ok());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub static IS_PORTABLE: Lazy<bool> = Lazy::new(|| {
|
||||
if cfg!(windows) {
|
||||
let dir = crate::utils::dirs::app_install_dir().unwrap();
|
||||
|
@@ -29,7 +29,7 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::api::process::Command;
|
||||
|
||||
use tokio::time::sleep;
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
@@ -414,17 +414,18 @@ impl CoreManager {
|
||||
let app_dir = dirs::app_data_dir()?;
|
||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||
log::debug!(target: "app", "check config in `{clash_core}`");
|
||||
let output = Command::new_sidecar(clash_core)?
|
||||
let output = std::process::Command::new(dirs::get_data_or_sidecar_path(&clash_core)?)
|
||||
.args(["-t", "-d", app_dir, "-f", config_path])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = api::parse_check_output(output.stdout.clone());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let error = api::parse_check_output(stdout.to_string());
|
||||
let error = match !error.is_empty() {
|
||||
true => error,
|
||||
false => output.stdout.clone(),
|
||||
false => stdout.to_string(),
|
||||
};
|
||||
Logger::global().set_log(output.stdout);
|
||||
Logger::global().set_log(stdout.to_string());
|
||||
bail!("{error}");
|
||||
}
|
||||
|
||||
@@ -462,11 +463,12 @@ impl CoreManager {
|
||||
|
||||
let tun_device_ip = Config::clash().clone().latest().get_tun_device_ip();
|
||||
// 执行 networksetup -setdnsservers Wi-Fi $tun_device_ip
|
||||
let (mut rx, _) = Command::new("networksetup")
|
||||
let output = tokio::process::Command::new("networksetup")
|
||||
.args(["-setdnsservers", "Wi-Fi", tun_device_ip.as_str()])
|
||||
.spawn()?;
|
||||
let event = rx.recv().await;
|
||||
log::debug!(target: "app", "{event:?}");
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
log::debug!(target: "app", "set system dns: {:?}", output);
|
||||
}
|
||||
}
|
||||
// FIXME: 重构服务模式
|
||||
@@ -553,9 +555,10 @@ impl CoreManager {
|
||||
if enable_tun {
|
||||
log::debug!(target: "app", "try to set system dns");
|
||||
|
||||
match Command::new("networksetup")
|
||||
match tokio::process::Command::new("networksetup")
|
||||
.args(["-setdnsservers", "Wi-Fi", "Empty"])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => {
|
||||
|
@@ -5,7 +5,7 @@ use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, Window};
|
||||
use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||
@@ -42,11 +42,11 @@ impl Handle {
|
||||
*self.app_handle.lock() = Some(app_handle);
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<Window> {
|
||||
pub fn get_window(&self) -> Option<WebviewWindow<Wry>> {
|
||||
self.app_handle
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|a| a.get_window("main"))
|
||||
.and_then(|a| a.get_webview_window("main"))
|
||||
}
|
||||
|
||||
pub fn refresh_clash() {
|
||||
@@ -81,11 +81,12 @@ impl Handle {
|
||||
}
|
||||
|
||||
pub fn update_systray() -> Result<()> {
|
||||
let app_handle = Self::global().app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("update_systray unhandled error");
|
||||
}
|
||||
Tray::update_systray(app_handle.as_ref().unwrap())?;
|
||||
// let app_handle = Self::global().app_handle.lock();
|
||||
// if app_handle.is_none() {
|
||||
// bail!("update_systray unhandled error");
|
||||
// }
|
||||
// Tray::update_systray(app_handle.as_ref().unwrap())?;
|
||||
Handle::emit("update_systray", ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,4 +99,14 @@ impl Handle {
|
||||
Tray::update_part(app_handle.as_ref().unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn emit<S: Serialize + Clone>(event: &str, payload: S) -> Result<()> {
|
||||
let app_handle = Self::global().app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("app_handle is not exist");
|
||||
}
|
||||
|
||||
app_handle.as_ref().unwrap().emit(event, payload)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -3,8 +3,9 @@ use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
@@ -64,24 +65,20 @@ impl Hotkey {
|
||||
fn check_key(hotkey: &str) -> Result<()> {
|
||||
// fix #287
|
||||
// tauri的这几个方法全部有Result expect,会panic,先检测一遍避免挂了
|
||||
if hotkey.parse::<Accelerator>().is_err() {
|
||||
if hotkey.parse::<Shortcut>().is_err() {
|
||||
bail!("invalid hotkey `{hotkey}`");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
|
||||
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let app_handle = self.app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("failed to get the hotkey manager");
|
||||
bail!("app handle is none");
|
||||
}
|
||||
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
|
||||
}
|
||||
let manager = app_handle.as_ref().unwrap().global_shortcut();
|
||||
|
||||
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let mut manager = self.get_manager()?;
|
||||
|
||||
if manager.is_registered(hotkey)? {
|
||||
if manager.is_registered(hotkey) {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
@@ -97,17 +94,25 @@ impl Hotkey {
|
||||
"toggle_tun_mode" => feat::toggle_tun_mode,
|
||||
"enable_tun_mode" => feat::enable_tun_mode,
|
||||
"disable_tun_mode" => feat::disable_tun_mode,
|
||||
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
};
|
||||
|
||||
manager.register(hotkey, f)?;
|
||||
manager.on_shortcut(hotkey, move |_app_handle, _hotkey, _ev| {
|
||||
f();
|
||||
})?;
|
||||
|
||||
log::info!(target: "app", "register hotkey {hotkey} {func}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
self.get_manager()?.unregister(hotkey)?;
|
||||
let app_handle = self.app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("app handle is none");
|
||||
}
|
||||
let manager = app_handle.as_ref().unwrap().global_shortcut();
|
||||
|
||||
manager.unregister(hotkey)?;
|
||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -189,8 +194,12 @@ impl Hotkey {
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut manager) = self.get_manager() {
|
||||
let _ = manager.unregister_all();
|
||||
let app_handle = self.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
let manager = app_handle.global_shortcut();
|
||||
if let Ok(()) = manager.unregister_all() {
|
||||
log::info!(target: "app", "unregister all hotkeys");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -216,6 +216,7 @@ pub async fn restart_service() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>> {
|
||||
let mut cmd = tokio::process::Command::new(SERVICE_PATH.as_path());
|
||||
cmd.args(["status", "--json"]);
|
||||
@@ -239,6 +240,6 @@ pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>>
|
||||
);
|
||||
}
|
||||
let mut status = String::from_utf8(output.stdout)?;
|
||||
tracing::debug!("service status: {}", status);
|
||||
tracing::trace!("service status: {}", status);
|
||||
Ok(unsafe { simd_json::serde::from_str(&mut status)? })
|
||||
}
|
||||
|
@@ -1,13 +1,18 @@
|
||||
use std::{borrow::Cow, sync::atomic::AtomicU16};
|
||||
|
||||
use crate::{
|
||||
config::{nyanpasu::ClashCore, Config},
|
||||
feat, ipc,
|
||||
feat, ipc, log_err,
|
||||
utils::{help, resolve},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use rust_i18n::t;
|
||||
use tauri::{
|
||||
AppHandle, CustomMenuItem, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
SystemTraySubmenu,
|
||||
menu::{Menu, MenuBuilder, MenuEvent, MenuItemBuilder, SubmenuBuilder},
|
||||
tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent},
|
||||
AppHandle, Manager, Runtime,
|
||||
};
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
@@ -15,12 +20,130 @@ pub mod icon;
|
||||
pub mod proxies;
|
||||
pub use self::icon::on_scale_factor_changed;
|
||||
use self::proxies::SystemTrayMenuProxiesExt;
|
||||
mod utils;
|
||||
|
||||
struct TrayState<R: Runtime> {
|
||||
menu: Mutex<Menu<R>>,
|
||||
}
|
||||
|
||||
pub struct Tray {}
|
||||
|
||||
static UPDATE_SYSTRAY_MUTEX: Lazy<parking_lot::Mutex<()>> =
|
||||
Lazy::new(|| parking_lot::Mutex::new(()));
|
||||
|
||||
const TRAY_ID: &str = "main-tray";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
static LINUX_TRAY_ID: AtomicU16 = AtomicU16::new(0);
|
||||
// #[cfg(target_os = "linux")]
|
||||
// fn bump_tray_id() -> Cow<'static, str> {
|
||||
// let id = LINUX_TRAY_ID.fetch_add(1, std::sync::atomic::Ordering::Release) + 1;
|
||||
// Cow::Owned(format!("{}-{}", TRAY_ID, id))
|
||||
// }
|
||||
|
||||
#[inline]
|
||||
fn get_tray_id<'n>() -> Cow<'n, str> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let id = LINUX_TRAY_ID.load(std::sync::atomic::Ordering::Acquire);
|
||||
Cow::Owned(format!("{}-{}", TRAY_ID, id))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Cow::Borrowed(TRAY_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// fn dummy_print_submenu<R: Runtime>(submenu: &Submenu<R>) {
|
||||
// for item in submenu.items().unwrap() {
|
||||
// tracing::debug!("item: {:#?}", item.id());
|
||||
// match item {
|
||||
// tauri::menu::MenuItemKind::MenuItem(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: MenuItem, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Submenu(submenu) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Submenu, text: {:#?}",
|
||||
// submenu.id(),
|
||||
// submenu.text()
|
||||
// );
|
||||
// dummy_print_submenu(&submenu);
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Predefined(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Predefined, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Check(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Check, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Icon(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Icon, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn dummy_print_menu<R: Runtime>(menu: &Menu<R>) {
|
||||
// for item in menu.items().unwrap() {
|
||||
// tracing::debug!("item: {:#?}", item.id());
|
||||
// match item {
|
||||
// tauri::menu::MenuItemKind::MenuItem(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: MenuItem, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Submenu(submenu) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Submenu, text: {:#?}",
|
||||
// submenu.id(),
|
||||
// submenu.text()
|
||||
// );
|
||||
// dummy_print_submenu(&submenu);
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Predefined(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Predefined, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Check(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Check, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// tauri::menu::MenuItemKind::Icon(item) => {
|
||||
// tracing::debug!(
|
||||
// "item: {:#?}, type: Icon, text: {:#?}",
|
||||
// item.id(),
|
||||
// item.text()
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Tray {
|
||||
#[instrument(skip(_app_handle))]
|
||||
pub fn tray_menu(_app_handle: &AppHandle) -> SystemTrayMenu {
|
||||
#[instrument(skip(app_handle))]
|
||||
pub fn tray_menu<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Menu<R>> {
|
||||
let version = env!("NYANPASU_VERSION");
|
||||
let core = {
|
||||
*Config::verge()
|
||||
@@ -29,72 +152,147 @@ impl Tray {
|
||||
.as_ref()
|
||||
.unwrap_or(&ClashCore::default())
|
||||
};
|
||||
let mut menu = SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", t!("tray.dashboard")))
|
||||
.setup_proxies() // Setup the proxies menu
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", t!("tray.rule_mode")))
|
||||
.add_item(CustomMenuItem::new("global_mode", t!("tray.global_mode")))
|
||||
.add_item(CustomMenuItem::new("direct_mode", t!("tray.direct_mode")));
|
||||
let mut menu = MenuBuilder::new(app_handle)
|
||||
.text("open_window", t!("tray.dashboard"))
|
||||
.setup_proxies(app_handle)? // Setup the proxies menu
|
||||
.separator()
|
||||
.check("rule_mode", t!("tray.rule_mode"))
|
||||
.check("global_mode", t!("tray.global_mode"))
|
||||
.check("direct_mode", t!("tray.direct_mode"));
|
||||
if core == ClashCore::ClashPremium {
|
||||
menu = menu.add_item(CustomMenuItem::new("script_mode", t!("tray.script_mode")))
|
||||
menu = menu.check("script_mode", t!("tray.script_mode"));
|
||||
}
|
||||
menu.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", t!("tray.system_proxy")))
|
||||
.add_item(CustomMenuItem::new("tun_mode", t!("tray.tun_mode")))
|
||||
.add_item(CustomMenuItem::new("copy_env_sh", t!("tray.copy_env.sh")))
|
||||
.add_item(CustomMenuItem::new("copy_env_cmd", t!("tray.copy_env.cmd")))
|
||||
.add_item(CustomMenuItem::new("copy_env_ps", t!("tray.copy_env.ps")))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("tray.open_dir.menu"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_app_config_dir",
|
||||
t!("tray.open_dir.app_config_dir"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_app_data_dir",
|
||||
t!("tray.open_dir.app_data_dir"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_core_dir",
|
||||
t!("tray.open_dir.core_dir"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_logs_dir",
|
||||
t!("tray.open_dir.log_dir"),
|
||||
)),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("tray.more.menu"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_clash",
|
||||
t!("tray.more.restart_clash"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_app",
|
||||
t!("tray.more.restart_app"),
|
||||
))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", t!("tray.quit")).accelerator("CmdOrControl+Q"))
|
||||
menu = menu
|
||||
.separator()
|
||||
.check("system_proxy", t!("tray.system_proxy"))
|
||||
.check("tun_mode", t!("tray.tun_mode"))
|
||||
.separator()
|
||||
.text("copy_env_sh", t!("tray.copy_env.sh"))
|
||||
.text("copy_env_cmd", t!("tray.copy_env.cmd"))
|
||||
.text("copy_env_ps", t!("tray.copy_env.ps"))
|
||||
.item(
|
||||
&SubmenuBuilder::new(app_handle, t!("tray.open_dir.menu"))
|
||||
.text("open_app_config_dir", t!("tray.open_dir.app_config_dir"))
|
||||
.text("open_app_data_dir", t!("tray.open_dir.app_data_dir"))
|
||||
.text("open_core_dir", t!("tray.open_dir.core_dir"))
|
||||
.text("open_logs_dir", t!("tray.open_dir.log_dir"))
|
||||
.build()?,
|
||||
)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app_handle, t!("tray.more.menu"))
|
||||
.text("restart_clash", t!("tray.more.restart_clash"))
|
||||
.text("restart_app", t!("tray.more.restart_app"))
|
||||
.item(
|
||||
&MenuItemBuilder::new(format!("Version {}", version))
|
||||
.id("app_version")
|
||||
.enabled(false)
|
||||
.build(app_handle)?,
|
||||
)
|
||||
.build()?,
|
||||
)
|
||||
.separator()
|
||||
.item(
|
||||
&MenuItemBuilder::new(t!("tray.quit"))
|
||||
.id("quit")
|
||||
.accelerator("CmdOrControl+Q")
|
||||
.build(app_handle)?,
|
||||
);
|
||||
|
||||
Ok(menu.build()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(app_handle))]
|
||||
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.set_menu(Tray::tray_menu(app_handle))?;
|
||||
pub fn update_systray(app_handle: &AppHandle<tauri::Wry>) -> Result<()> {
|
||||
let _guard = UPDATE_SYSTRAY_MUTEX.lock();
|
||||
let tray_id = get_tray_id();
|
||||
let tray = {
|
||||
// if cfg!(target_os = "linux") {
|
||||
// tracing::debug!("removing tray by id: {}", tray_id);
|
||||
// let mut tray = app_handle.remove_tray_by_id(tray_id.as_ref());
|
||||
// tray.take(); // Drop the tray
|
||||
// tray_id = bump_tray_id();
|
||||
// tracing::debug!("bumped tray id to: {}", tray_id);
|
||||
// }
|
||||
app_handle.tray_by_id(tray_id.as_ref())
|
||||
};
|
||||
|
||||
let menu = Tray::tray_menu(app_handle)?;
|
||||
let tray = match tray {
|
||||
None => {
|
||||
let mut builder = TrayIconBuilder::with_id(tray_id);
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
{
|
||||
builder = builder.icon(tauri::image::Image::from_bytes(&icon::get_icon(
|
||||
&icon::TrayIcon::Normal,
|
||||
))?);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../../../icons/tray-icon.png"
|
||||
))?)
|
||||
.icon_as_template(true);
|
||||
}
|
||||
builder
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| {
|
||||
Tray::on_menu_item_event(app, event);
|
||||
})
|
||||
.on_tray_icon_event(|tray_icon, event| {
|
||||
Tray::on_system_tray_event(tray_icon, event);
|
||||
})
|
||||
.build(app_handle)?
|
||||
}
|
||||
Some(tray) => {
|
||||
// This is a workaround for linux tray menu update. Due to the api disallow set_menu again
|
||||
// and recreate tray icon will cause buggy tray. No icon and no menu.
|
||||
// So this block is a dirty inheritance of the menu items from the previous tray menu.
|
||||
if cfg!(target_os = "linux") {
|
||||
let state = app_handle.state::<TrayState<tauri::Wry>>();
|
||||
let previous_menu = state.menu.lock();
|
||||
if let Ok(items) = previous_menu.items() {
|
||||
tracing::debug!("removing previous tray menu items");
|
||||
for item in items {
|
||||
log_err!(previous_menu.remove(&item), "failed to remove menu item");
|
||||
}
|
||||
}
|
||||
// migrate the menu items
|
||||
if let Ok(items) = menu.items() {
|
||||
tracing::debug!("migrating new tray menu items");
|
||||
for item in items {
|
||||
log_err!(previous_menu.append(&item), "failed to append menu item");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tray.set_menu(Some(menu.clone()))?;
|
||||
}
|
||||
tray
|
||||
}
|
||||
};
|
||||
tray.set_visible(true)?;
|
||||
{
|
||||
match app_handle.try_state::<TrayState<tauri::Wry>>() {
|
||||
Some(state) if cfg!(not(target_os = "linux")) => {
|
||||
tracing::debug!("replacing previous tray menu");
|
||||
*state.menu.lock() = menu;
|
||||
}
|
||||
None => {
|
||||
tracing::debug!("creating new tray menu");
|
||||
app_handle.manage(TrayState {
|
||||
menu: Mutex::new(menu),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
tracing::debug!("full update tray finished");
|
||||
Tray::update_part(app_handle)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(app_handle))]
|
||||
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
|
||||
pub fn update_part<R: Runtime>(app_handle: &AppHandle<R>) -> Result<()> {
|
||||
let mode = crate::utils::config::get_current_clash_mode();
|
||||
let core = {
|
||||
*Config::verge()
|
||||
@@ -103,54 +301,25 @@ impl Tray {
|
||||
.as_ref()
|
||||
.unwrap_or(&ClashCore::default())
|
||||
};
|
||||
let tray = app_handle.tray_handle();
|
||||
let tray_id = get_tray_id();
|
||||
tracing::debug!("updating tray part: {}", tray_id);
|
||||
let tray = app_handle.tray_by_id(tray_id.as_ref()).unwrap();
|
||||
let state = app_handle.state::<TrayState<R>>();
|
||||
let menu = state.menu.lock();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = tray.get_item("rule_mode").set_title(t!("tray.rule_mode"));
|
||||
let _ = tray
|
||||
.get_item("global_mode")
|
||||
.set_title(t!("tray.global_mode"));
|
||||
let _ = tray
|
||||
.get_item("direct_mode")
|
||||
.set_title(t!("tray.direct_mode"));
|
||||
if core == ClashCore::ClashPremium {
|
||||
let _ = tray
|
||||
.get_item("script_mode")
|
||||
.set_title(t!("tray.script_mode"));
|
||||
}
|
||||
match mode.as_str() {
|
||||
"rule" => {
|
||||
let _ = tray
|
||||
.get_item("rule_mode")
|
||||
.set_title(utils::selected_title(t!("tray.rule_mode")));
|
||||
}
|
||||
"global" => {
|
||||
let _ = tray
|
||||
.get_item("global_mode")
|
||||
.set_title(utils::selected_title(t!("tray.global_mode")));
|
||||
}
|
||||
"direct" => {
|
||||
let _ = tray
|
||||
.get_item("direct_mode")
|
||||
.set_title(utils::selected_title(t!("tray.direct_mode")));
|
||||
}
|
||||
"script" => {
|
||||
let _ = tray
|
||||
.get_item("script_mode")
|
||||
.set_title(utils::selected_title(t!("tray.script_mode")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
|
||||
let _ = tray.get_item("global_mode").set_selected(mode == "global");
|
||||
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
|
||||
if core == ClashCore::ClashPremium {
|
||||
let _ = tray.get_item("script_mode").set_selected(mode == "script");
|
||||
}
|
||||
let _ = menu
|
||||
.get("rule_mode")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "rule").ok());
|
||||
let _ = menu
|
||||
.get("global_mode")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "global").ok());
|
||||
let _ = menu
|
||||
.get("direct_mode")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "direct").ok());
|
||||
if core == ClashCore::ClashPremium {
|
||||
let _ = menu
|
||||
.get("script_mode")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(mode == "script").ok());
|
||||
}
|
||||
|
||||
let (system_proxy, tun_mode) = {
|
||||
@@ -162,7 +331,7 @@ impl Tray {
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
use icon::TrayIcon;
|
||||
|
||||
@@ -174,98 +343,86 @@ impl Tray {
|
||||
TrayIcon::Normal
|
||||
};
|
||||
let icon = icon::get_icon(&mode);
|
||||
let _ = tray.set_icon(tauri::Icon::Raw(icon));
|
||||
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon)?));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
match system_proxy {
|
||||
true => {
|
||||
let _ = tray
|
||||
.get_item("system_proxy")
|
||||
.set_title(utils::selected_title(t!("tray.system_proxy")));
|
||||
}
|
||||
false => {
|
||||
let _ = tray
|
||||
.get_item("system_proxy")
|
||||
.set_title(t!("tray.system_proxy"));
|
||||
}
|
||||
}
|
||||
let _ = menu
|
||||
.get("system_proxy")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(system_proxy).ok());
|
||||
let _ = menu
|
||||
.get("tun_mode")
|
||||
.and_then(|item| item.as_check_menuitem()?.set_checked(tun_mode).ok());
|
||||
|
||||
match tun_mode {
|
||||
true => {
|
||||
let _ = tray
|
||||
.get_item("tun_mode")
|
||||
.set_title(utils::selected_title(t!("tray.tun_mode")));
|
||||
}
|
||||
false => {
|
||||
let _ = tray.get_item("tun_mode").set_title(t!("tray.tun_mode"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let switch_map = {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(true, t!("tray.proxy_action.on"));
|
||||
map.insert(false, t!("tray.proxy_action.off"));
|
||||
map
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = tray.get_item("system_proxy").set_selected(system_proxy);
|
||||
let _ = tray.get_item("tun_mode").set_selected(tun_mode);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let switch_map = {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert(true, t!("tray.proxy_action.on"));
|
||||
map.insert(false, t!("tray.proxy_action.off"));
|
||||
map
|
||||
};
|
||||
|
||||
let _ = tray.set_tooltip(&format!(
|
||||
let _ = tray.set_tooltip(Some(&format!(
|
||||
"{}: {}\n{}: {}",
|
||||
t!("tray.system_proxy"),
|
||||
switch_map[&system_proxy],
|
||||
t!("tray.tun_mode"),
|
||||
switch_map[&tun_mode]
|
||||
));
|
||||
)));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = tray.set_title(Some(&format!(
|
||||
"{}: {}\n{}: {}",
|
||||
t!("tray.system_proxy"),
|
||||
switch_map[&system_proxy],
|
||||
t!("tray.tun_mode"),
|
||||
switch_map[&tun_mode]
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(app_handle, event))]
|
||||
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||
match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
|
||||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env_sh" => feat::copy_clash_env("sh"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_cmd" => feat::copy_clash_env("cmd"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_ps" => feat::copy_clash_env("ps"),
|
||||
"open_app_config_dir" => crate::log_err!(ipc::open_app_config_dir()),
|
||||
"open_app_data_dir" => crate::log_err!(ipc::open_app_data_dir()),
|
||||
"open_core_dir" => crate::log_err!(ipc::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(ipc::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => help::restart_application(app_handle),
|
||||
"quit" => {
|
||||
help::quit_application(app_handle);
|
||||
}
|
||||
_ => {
|
||||
proxies::on_system_tray_event(&id);
|
||||
}
|
||||
},
|
||||
#[cfg(target_os = "windows")]
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
resolve::create_window(app_handle);
|
||||
pub fn on_menu_item_event(app_handle: &AppHandle, event: MenuEvent) {
|
||||
let id = event.id().0.as_str();
|
||||
match id {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
_ => {}
|
||||
|
||||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env_sh" => feat::copy_clash_env(app_handle, "sh"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_cmd" => feat::copy_clash_env(app_handle, "cmd"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_ps" => feat::copy_clash_env(app_handle, "ps"),
|
||||
"open_app_config_dir" => crate::log_err!(ipc::open_app_config_dir()),
|
||||
"open_app_data_dir" => crate::log_err!(ipc::open_app_data_dir()),
|
||||
"open_core_dir" => crate::log_err!(ipc::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(ipc::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => help::restart_application(app_handle),
|
||||
"quit" => {
|
||||
help::quit_application(app_handle);
|
||||
}
|
||||
_ => {
|
||||
proxies::on_system_tray_event(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_system_tray_event(tray_icon: &TrayIcon, event: TrayIconEvent) {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
resolve::create_window(tray_icon.app_handle());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
|
||||
use indexmap::IndexMap;
|
||||
use tauri::SystemTrayMenu;
|
||||
use tauri::{menu::MenuBuilder, AppHandle, Manager, Runtime};
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
@@ -41,8 +40,9 @@ async fn loop_task() {
|
||||
}
|
||||
|
||||
type GroupName = String;
|
||||
type FromProxy = String;
|
||||
type ToProxy = String;
|
||||
type ProxyName = String;
|
||||
type FromProxy = ProxyName;
|
||||
type ToProxy = ProxyName;
|
||||
type ProxySelectAction = (GroupName, FromProxy, ToProxy);
|
||||
#[derive(PartialEq)]
|
||||
enum TrayUpdateType {
|
||||
@@ -179,8 +179,9 @@ pub async fn proxies_updated_receiver() {
|
||||
match diff_proxies(&tray_proxies_holder, ¤t_tray_proxies) {
|
||||
TrayUpdateType::Full => {
|
||||
debug!("should do full update");
|
||||
|
||||
tray_proxies_holder = current_tray_proxies;
|
||||
match Handle::update_systray() {
|
||||
match Handle::emit("update_systray", ()) {
|
||||
Ok(_) => {
|
||||
debug!("update systray success");
|
||||
}
|
||||
@@ -211,91 +212,129 @@ pub fn setup_proxies() {
|
||||
}
|
||||
|
||||
mod platform_impl {
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use super::{ProxySelectAction, TrayProxyItem};
|
||||
use super::{GroupName, ProxyName, ProxySelectAction, TrayProxyItem};
|
||||
use crate::{
|
||||
config::nyanpasu::ProxiesSelectorMode,
|
||||
core::{clash::proxies::ProxiesGuard, handle::Handle},
|
||||
};
|
||||
use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _};
|
||||
use bimap::BiMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use rust_i18n::t;
|
||||
use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use tauri::{
|
||||
menu::{
|
||||
CheckMenuItemBuilder, IsMenuItem, Menu, MenuBuilder, MenuItemBuilder, MenuItemKind,
|
||||
Submenu, SubmenuBuilder,
|
||||
},
|
||||
AppHandle, Manager, Runtime,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
pub fn generate_group_selector(group_name: &str, group: &TrayProxyItem) -> SystemTraySubmenu {
|
||||
let mut group_menu = SystemTrayMenu::new();
|
||||
for item in group.all.iter() {
|
||||
let mut sub_item = CustomMenuItem::new(
|
||||
format!(
|
||||
"select_proxy_{}_{}",
|
||||
base64_standard.encode(group_name),
|
||||
base64_standard.encode(item)
|
||||
),
|
||||
item.clone(),
|
||||
// It store a map of proxy nodes like "GROUP_PROXY" -> ID
|
||||
// TODO: use Cow<str> instead of String
|
||||
pub(super) static ITEM_IDS: Lazy<Mutex<BiMap<(GroupName, ProxyName), usize>>> =
|
||||
Lazy::new(|| Mutex::new(BiMap::new()));
|
||||
|
||||
pub fn generate_group_selector<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
group_name: &str,
|
||||
group: &TrayProxyItem,
|
||||
) -> anyhow::Result<Submenu<R>> {
|
||||
let mut item_ids = ITEM_IDS.lock();
|
||||
let mut group_menu = SubmenuBuilder::new(app_handle, group_name);
|
||||
if group.all.is_empty() {
|
||||
group_menu = group_menu.item(
|
||||
&MenuItemBuilder::new(t!("tray.no_proxies"))
|
||||
.enabled(false)
|
||||
.build(app_handle)?,
|
||||
);
|
||||
return Ok(group_menu.build()?);
|
||||
}
|
||||
for item in group.all.iter() {
|
||||
let key = (group_name.to_string(), item.to_string());
|
||||
let id = item_ids.len();
|
||||
item_ids.insert(key, id);
|
||||
let mut sub_item_builder = CheckMenuItemBuilder::new(item.clone())
|
||||
.id(format!("proxy_node_{}", id))
|
||||
.checked(false);
|
||||
if let Some(now) = group.current.clone() {
|
||||
if now == item.as_str() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
sub_item.title = super::super::utils::selected_title(item);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
sub_item = sub_item.selected();
|
||||
}
|
||||
sub_item_builder = sub_item_builder.checked(true);
|
||||
}
|
||||
}
|
||||
|
||||
if !matches!(group.r#type.as_str(), "Selector" | "Fallback") {
|
||||
sub_item = sub_item.disabled();
|
||||
sub_item_builder = sub_item_builder.enabled(false);
|
||||
}
|
||||
|
||||
group_menu = group_menu.add_item(sub_item);
|
||||
group_menu = group_menu.item(&sub_item_builder.build(app_handle)?);
|
||||
}
|
||||
SystemTraySubmenu::new(group_name.to_string(), group_menu)
|
||||
Ok(group_menu.build()?)
|
||||
}
|
||||
|
||||
pub fn generate_selectors(
|
||||
menu: &SystemTrayMenu,
|
||||
pub fn generate_selectors<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
proxies: &super::TrayProxies,
|
||||
) -> SystemTrayMenu {
|
||||
let mut menu = menu.to_owned();
|
||||
) -> anyhow::Result<Vec<MenuItemKind<R>>> {
|
||||
let mut items = Vec::new();
|
||||
if proxies.is_empty() {
|
||||
return menu.add_item(CustomMenuItem::new("no_proxies", "No Proxies"));
|
||||
items.push(MenuItemKind::MenuItem(
|
||||
MenuItemBuilder::new("No Proxies")
|
||||
.id("no_proxies")
|
||||
.enabled(false)
|
||||
.build(app_handle)?,
|
||||
));
|
||||
return Ok(items);
|
||||
}
|
||||
{
|
||||
let mut item_ids = ITEM_IDS.lock();
|
||||
item_ids.clear(); // clear the item ids
|
||||
}
|
||||
for (group, item) in proxies.iter() {
|
||||
let group_menu = generate_group_selector(group, item);
|
||||
menu = menu.add_submenu(group_menu);
|
||||
let group_menu = generate_group_selector(app_handle, group, item)?;
|
||||
items.push(MenuItemKind::Submenu(group_menu));
|
||||
}
|
||||
menu
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn setup_tray(menu: &mut SystemTrayMenu) -> SystemTrayMenu {
|
||||
let mut parent_menu = menu.to_owned();
|
||||
pub fn setup_tray<'m, R: Runtime, M: Manager<R>>(
|
||||
app_handle: &AppHandle<R>,
|
||||
mut menu: MenuBuilder<'m, R, M>,
|
||||
) -> anyhow::Result<MenuBuilder<'m, R, M>> {
|
||||
let selector_mode = crate::config::Config::verge()
|
||||
.latest()
|
||||
.clash_tray_selector
|
||||
.unwrap_or_default();
|
||||
let mut menu = match selector_mode {
|
||||
ProxiesSelectorMode::Hidden => return parent_menu,
|
||||
ProxiesSelectorMode::Normal => {
|
||||
parent_menu = parent_menu.add_native_item(SystemTrayMenuItem::Separator);
|
||||
parent_menu.clone()
|
||||
}
|
||||
ProxiesSelectorMode::Submenu => SystemTrayMenu::new(),
|
||||
menu = match selector_mode {
|
||||
ProxiesSelectorMode::Hidden => return Ok(menu),
|
||||
ProxiesSelectorMode::Normal => menu.separator(),
|
||||
ProxiesSelectorMode::Submenu => menu,
|
||||
};
|
||||
let proxies = ProxiesGuard::global().read().inner().to_owned();
|
||||
let mode = crate::utils::config::get_current_clash_mode();
|
||||
let tray_proxies = super::to_tray_proxies(mode.as_str(), &proxies);
|
||||
menu = generate_selectors(&menu, &tray_proxies);
|
||||
if selector_mode == ProxiesSelectorMode::Submenu {
|
||||
parent_menu =
|
||||
parent_menu.add_submenu(SystemTraySubmenu::new(t!("tray.select_proxies"), menu));
|
||||
parent_menu
|
||||
} else {
|
||||
menu
|
||||
let items = generate_selectors::<R>(app_handle, &tray_proxies)?;
|
||||
match selector_mode {
|
||||
ProxiesSelectorMode::Normal => {
|
||||
for item in items {
|
||||
menu = menu.item(&item);
|
||||
}
|
||||
}
|
||||
ProxiesSelectorMode::Submenu => {
|
||||
let mut submenu = SubmenuBuilder::with_id(
|
||||
app_handle,
|
||||
"select_proxies",
|
||||
t!("tray.select_proxies"),
|
||||
);
|
||||
for item in items {
|
||||
submenu = submenu.item(&item);
|
||||
}
|
||||
menu = menu.item(&submenu.build()?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
static TRAY_ITEM_UPDATE_BARRIER: AtomicBool = AtomicBool::new(false);
|
||||
@@ -306,84 +345,175 @@ mod platform_impl {
|
||||
warn!("tray item update is in progress, skip this update");
|
||||
return;
|
||||
}
|
||||
let tray = Handle::global()
|
||||
.app_handle
|
||||
.lock()
|
||||
let app_handle = Handle::global().app_handle.lock();
|
||||
let tray_state = app_handle
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.tray_handle();
|
||||
.state::<crate::core::tray::TrayState<tauri::Wry>>();
|
||||
TRAY_ITEM_UPDATE_BARRIER.store(true, std::sync::atomic::Ordering::Release);
|
||||
let menu = tray_state.menu.lock();
|
||||
let item_ids = ITEM_IDS.lock();
|
||||
for action in actions {
|
||||
tracing::debug!("update selected proxies: {:?}", action);
|
||||
let from = format!(
|
||||
"select_proxy_{}_{}",
|
||||
base64_standard.encode(&action.0),
|
||||
base64_standard.encode(&action.1)
|
||||
);
|
||||
let to = format!(
|
||||
"select_proxy_{}_{}",
|
||||
base64_standard.encode(&action.0),
|
||||
base64_standard.encode(&action.2)
|
||||
);
|
||||
// #[cfg(not(target_os = "linux"))]
|
||||
// {
|
||||
// tracing::debug!("update selected proxies: {:?}", action);
|
||||
// let from_id = match item_ids.get_by_left(&(action.0.clone(), action.1.clone())) {
|
||||
// Some(id) => *id,
|
||||
// None => {
|
||||
// warn!("from item not found: {:?}", action);
|
||||
// continue;
|
||||
// }
|
||||
// };
|
||||
// let from_id = format!("proxy_node_{}", from_id);
|
||||
|
||||
match tray.try_get_item(&from) {
|
||||
// let to_id = match item_ids.get_by_left(&(action.0.clone(), action.2.clone())) {
|
||||
// Some(id) => *id,
|
||||
// None => {
|
||||
// warn!("to item not found: {:?}", action);
|
||||
// continue;
|
||||
// }
|
||||
// };
|
||||
// let to_id = format!("proxy_node_{}", to_id);
|
||||
|
||||
// match menu.get(&from_id) {
|
||||
// Some(item) => match item.kind() {
|
||||
// MenuItemKind::Check(item) => {
|
||||
// if item.is_checked().is_ok_and(|x| x) {
|
||||
// let _ = item.set_checked(false);
|
||||
// }
|
||||
// }
|
||||
// MenuItemKind::MenuItem(item) => {
|
||||
// let _ = item.set_text(action.1.clone());
|
||||
// }
|
||||
// _ => {
|
||||
// warn!("failed to deselect, item is not a check item: {}", from_id);
|
||||
// }
|
||||
// },
|
||||
// None => {
|
||||
// warn!("failed to deselect, item not found: {}", from_id);
|
||||
// }
|
||||
// }
|
||||
// match menu.get(&to_id) {
|
||||
// Some(item) => match item.kind() {
|
||||
// MenuItemKind::Check(item) => {
|
||||
// if item.is_checked().is_ok_and(|x| !x) {
|
||||
// let _ = item.set_checked(true);
|
||||
// }
|
||||
// }
|
||||
// MenuItemKind::MenuItem(item) => {
|
||||
// let _ = item.set_text(action.2.clone());
|
||||
// }
|
||||
// _ => {
|
||||
// warn!("failed to select, item is not a check item: {}", to_id);
|
||||
// }
|
||||
// },
|
||||
// None => {
|
||||
// warn!("failed to select, item not found: {}", to_id);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// here is a fucking workaround for id getter
|
||||
#[inline]
|
||||
fn find_check_item<R: Runtime>(
|
||||
menu: &Menu<R>,
|
||||
group: GroupName,
|
||||
proxy: ProxyName,
|
||||
) -> Option<tauri::menu::CheckMenuItem<R>> {
|
||||
menu.items()
|
||||
.ok()
|
||||
.and_then(|items| {
|
||||
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group) || submenu.id() == "select_proxies"))
|
||||
})
|
||||
.and_then(|submenu| {
|
||||
let submenu = submenu.as_submenu_unchecked();
|
||||
if submenu.id() == "select_proxies" {
|
||||
submenu.items().ok().and_then(|items| {
|
||||
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Submenu(submenu) if submenu.text().is_ok_and(|text| text == group)))
|
||||
})
|
||||
.and_then(|submenu| {
|
||||
submenu.as_submenu_unchecked().items().ok()
|
||||
})
|
||||
} else {
|
||||
submenu.items().ok()
|
||||
}
|
||||
})
|
||||
.and_then(|items| {
|
||||
items.into_iter().find(|i| matches!(i, tauri::menu::MenuItemKind::Check(item) if item.text().is_ok_and(|text| text == proxy)))
|
||||
}).map(|item| item.as_check_menuitem_unchecked().clone())
|
||||
}
|
||||
|
||||
let from_item = find_check_item(&menu, actions[0].0.clone(), actions[0].1.clone());
|
||||
match from_item {
|
||||
Some(item) => {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = item.set_selected(false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = item.set_title(action.1.clone());
|
||||
}
|
||||
let _ = item.set_checked(false);
|
||||
}
|
||||
None => {
|
||||
warn!("failed to deselect, item not found: {}", from);
|
||||
warn!(
|
||||
"failed to deselect, item not found: {} {}",
|
||||
actions[0].0, actions[0].1
|
||||
);
|
||||
}
|
||||
}
|
||||
match tray.try_get_item(&to) {
|
||||
|
||||
let to_item = find_check_item(&menu, actions[0].0.clone(), actions[0].2.clone());
|
||||
match to_item {
|
||||
Some(item) => {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = item.set_selected(true);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = item.set_title(super::super::utils::selected_title(&action.2));
|
||||
}
|
||||
let _ = item.set_checked(true);
|
||||
}
|
||||
None => {
|
||||
warn!("failed to select, item not found: {}", to);
|
||||
warn!(
|
||||
"failed to select, item not found: {} {}",
|
||||
actions[0].0, actions[0].2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TRAY_ITEM_UPDATE_BARRIER.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
TRAY_ITEM_UPDATE_BARRIER.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SystemTrayMenuProxiesExt {
|
||||
fn setup_proxies(&mut self) -> Self;
|
||||
pub trait SystemTrayMenuProxiesExt<R: Runtime> {
|
||||
fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl SystemTrayMenuProxiesExt for SystemTrayMenu {
|
||||
fn setup_proxies(&mut self) -> Self {
|
||||
platform_impl::setup_tray(self)
|
||||
impl<'m, R: Runtime, M: Manager<R>> SystemTrayMenuProxiesExt<R> for MenuBuilder<'m, R, M> {
|
||||
fn setup_proxies(self, app_handle: &AppHandle<R>) -> anyhow::Result<Self> {
|
||||
platform_impl::setup_tray(app_handle, self)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub fn on_system_tray_event(event: &str) {
|
||||
if !event.starts_with("select_proxy_") {
|
||||
if !event.starts_with("proxy_node_") {
|
||||
return; // bypass non-select event
|
||||
}
|
||||
let parts: Vec<&str> = event.split('_').collect();
|
||||
if parts.len() != 4 {
|
||||
return; // bypass invalid event
|
||||
}
|
||||
let node_id = event.split('_').last().unwrap(); // safe to unwrap
|
||||
let node_id = match node_id.parse::<usize>() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("parse node id failed: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (group, name) = {
|
||||
let map = platform_impl::ITEM_IDS.lock();
|
||||
let item = map.get_by_right(&node_id);
|
||||
match item {
|
||||
Some((group, name)) => (group.clone(), name.clone()),
|
||||
None => {
|
||||
error!("node id not found: {}", node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let wrapper = move || -> anyhow::Result<()> {
|
||||
let group = String::from_utf8(base64_standard.decode(parts[2])?)?;
|
||||
let name = String::from_utf8(base64_standard.decode(parts[3])?)?;
|
||||
tracing::debug!("received select proxy event: {} {}", group, name);
|
||||
tauri::async_runtime::block_on(async move {
|
||||
ProxiesGuard::global()
|
||||
|
@@ -1,3 +0,0 @@
|
||||
pub(super) fn selected_title(s: impl AsRef<str>) -> String {
|
||||
format!("{} ✔", s.as_ref())
|
||||
}
|
@@ -22,7 +22,7 @@ macro_rules! append {
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[tracing_attributes::instrument(skip(config))]
|
||||
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
let tun_key = Value::from("tun");
|
||||
let tun_val = config.get(&tun_key);
|
||||
|
@@ -14,7 +14,8 @@ use anyhow::{bail, Result};
|
||||
use handle::Message;
|
||||
use nyanpasu_ipc::api::status::CoreState;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use wry::application::clipboard::Clipboard;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
// 打开面板
|
||||
#[allow(unused)]
|
||||
@@ -400,7 +401,7 @@ async fn update_core_config() -> Result<()> {
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env(option: &str) {
|
||||
pub fn copy_clash_env(app_handle: &AppHandle, option: &str) {
|
||||
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7890) };
|
||||
let http_proxy = format!("http://127.0.0.1:{}", port);
|
||||
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
||||
@@ -410,12 +411,24 @@ pub fn copy_clash_env(option: &str) {
|
||||
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
|
||||
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||
|
||||
let mut clipboard = Clipboard::new();
|
||||
let clipboard = app_handle.clipboard();
|
||||
|
||||
match option {
|
||||
"sh" => clipboard.write_text(sh),
|
||||
"cmd" => clipboard.write_text(cmd),
|
||||
"ps" => clipboard.write_text(ps),
|
||||
"sh" => {
|
||||
if let Err(e) = clipboard.write_text(sh) {
|
||||
log::error!(target: "app", "copy_clash_env failed: {e}");
|
||||
}
|
||||
}
|
||||
"cmd" => {
|
||||
if let Err(e) = clipboard.write_text(cmd) {
|
||||
log::error!(target: "app", "copy_clash_env failed: {e}");
|
||||
}
|
||||
}
|
||||
"ps" => {
|
||||
if let Err(e) = clipboard.write_text(ps) {
|
||||
log::error!(target: "app", "copy_clash_env failed: {e}");
|
||||
}
|
||||
}
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
|
||||
}
|
||||
}
|
||||
|
@@ -20,9 +20,10 @@ use profile::item_type::ProfileItemType;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{borrow::Cow, collections::VecDeque, path::PathBuf};
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::AppHandle;
|
||||
use tray::icon::TrayIcon;
|
||||
|
||||
use tauri::api::dialog::FileDialogBuilder;
|
||||
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
@@ -339,34 +340,36 @@ pub async fn fetch_latest_core_versions() -> CmdResult<ManifestVersionLatest> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_core_version(core_type: nyanpasu::ClashCore) -> CmdResult<String> {
|
||||
match tokio::task::spawn_blocking(move || resolve::resolve_core_version(&core_type)).await {
|
||||
Ok(Ok(version)) => Ok(version),
|
||||
Ok(Err(err)) => Err(format!("{err}")),
|
||||
pub async fn get_core_version(
|
||||
app_handle: AppHandle,
|
||||
core_type: nyanpasu::ClashCore,
|
||||
) -> CmdResult<String> {
|
||||
match resolve::resolve_core_version(&app_handle, &core_type).await {
|
||||
Ok(version) => Ok(version),
|
||||
Err(err) => Err(format!("{err}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn collect_logs() -> CmdResult {
|
||||
pub async fn collect_logs(app_handle: AppHandle) -> CmdResult {
|
||||
let now = Local::now().format("%Y-%m-%d");
|
||||
let fname = format!("{}-log", now);
|
||||
let builder = FileDialogBuilder::new();
|
||||
let builder = FileDialogBuilder::new(app_handle.dialog().clone());
|
||||
builder
|
||||
.add_filter("archive files", &["zip"])
|
||||
.set_file_name(&fname)
|
||||
.set_title("Save log archive")
|
||||
.save_file(|file_path| match file_path {
|
||||
None => (),
|
||||
Some(path) => {
|
||||
debug!("{:#?}", path.as_os_str());
|
||||
match candy::collect_logs(&path) {
|
||||
Some(path) if path.as_path().is_some() => {
|
||||
debug!("{:#?}", path);
|
||||
match candy::collect_logs(path.as_path().unwrap()) {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -652,3 +655,9 @@ pub async fn get_service_install_prompt() -> CmdResult<String> {
|
||||
}
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cleanup_processes(app_handle: AppHandle) -> CmdResult {
|
||||
crate::utils::help::cleanup_processes(&app_handle);
|
||||
Ok(())
|
||||
}
|
||||
|
353
clash-nyanpasu/backend/tauri/src/lib.rs
Normal file
353
clash-nyanpasu/backend/tauri/src/lib.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
#![feature(auto_traits, negative_impls)]
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod consts;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod ipc;
|
||||
mod server;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::handle::Handle,
|
||||
utils::{init, resolve},
|
||||
};
|
||||
use tauri::Emitter;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use utils::resolve::{is_window_opened, reset_window_open_counter};
|
||||
|
||||
rust_i18n::i18n!("../../locales");
|
||||
|
||||
#[cfg(feature = "deadlock-detection")]
|
||||
fn deadlock_detection() {
|
||||
use parking_lot::deadlock;
|
||||
use std::{thread, time::Duration};
|
||||
use tracing::error;
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
error!("Deadlock #{}", i);
|
||||
for t in threads {
|
||||
error!("Thread Id {:#?}", t.thread_id());
|
||||
error!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() -> std::io::Result<()> {
|
||||
// share the tauri async runtime to nyanpasu-utils
|
||||
#[cfg(feature = "deadlock-detection")]
|
||||
deadlock_detection();
|
||||
|
||||
// Should be in first place in order prevent single instance check block everything
|
||||
// Custom scheme check
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
// on macos the plugin handles this (macos doesn't use cli args for the url)
|
||||
let custom_scheme = match std::env::args().nth(1) {
|
||||
Some(url) => url::Url::parse(&url).ok(),
|
||||
None => None,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let custom_scheme: Option<url::Url> = None;
|
||||
|
||||
if custom_scheme.is_none() {
|
||||
// Parse commands
|
||||
cmds::parse().unwrap();
|
||||
};
|
||||
#[cfg(feature = "verge-dev")]
|
||||
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu.dev");
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu");
|
||||
|
||||
// 单例检测
|
||||
let single_instance_result = utils::init::check_singleton();
|
||||
if single_instance_result
|
||||
.as_ref()
|
||||
.is_ok_and(|instance| instance.is_none())
|
||||
{
|
||||
std::process::exit(0);
|
||||
}
|
||||
// Use system locale as default
|
||||
let locale = {
|
||||
let locale = utils::help::get_system_locale();
|
||||
utils::help::mapping_to_i18n_key(&locale)
|
||||
};
|
||||
rust_i18n::set_locale(locale);
|
||||
|
||||
if single_instance_result
|
||||
.as_ref()
|
||||
.is_ok_and(|instance| instance.is_some())
|
||||
{
|
||||
if let Err(e) = init::run_pending_migrations() {
|
||||
utils::dialog::panic_dialog(
|
||||
&format!(
|
||||
"Failed to finish migration event: {}\nYou can see the detailed information at migration.log in your local data dir.\nYou're supposed to submit it as the attachment of new issue.",
|
||||
e,
|
||||
)
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
crate::log_err!(init::init_config());
|
||||
|
||||
// Panic Hook to show a panic dialog and save logs
|
||||
let default_panic = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
use std::backtrace::{Backtrace, BacktraceStatus};
|
||||
let payload = panic_info.payload();
|
||||
|
||||
#[allow(clippy::manual_map)]
|
||||
let payload = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
Some(&**s)
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
Some(s.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let location = panic_info.location().map(|l| l.to_string());
|
||||
let (backtrace, note) = {
|
||||
let backtrace = Backtrace::capture();
|
||||
let note = (backtrace.status() == BacktraceStatus::Disabled)
|
||||
.then_some("run with RUST_BACKTRACE=1 environment variable to display a backtrace");
|
||||
(Some(backtrace), note)
|
||||
};
|
||||
|
||||
tracing::error!(
|
||||
panic.payload = payload,
|
||||
panic.location = location,
|
||||
panic.backtrace = backtrace.as_ref().map(tracing::field::display),
|
||||
panic.note = note,
|
||||
"A panic occurred",
|
||||
);
|
||||
utils::dialog::panic_dialog(&format!(
|
||||
"payload: {:#?}\nlocation: {:?}\nbacktrace: {:#?}\n\nnote: {:?}",
|
||||
payload, location, backtrace, note
|
||||
));
|
||||
|
||||
// cleanup the core manager
|
||||
let task = std::thread::spawn(move || {
|
||||
nyanpasu_utils::runtime::block_on(async {
|
||||
let _ = crate::core::CoreManager::global().stop_core().await;
|
||||
});
|
||||
});
|
||||
let _ = task.join();
|
||||
default_panic(panic_info);
|
||||
}));
|
||||
|
||||
let verge = { Config::verge().latest().language.clone().unwrap() };
|
||||
rust_i18n::set_locale(verge.as_str());
|
||||
|
||||
// show a dialog to print the single instance error
|
||||
let _singleton = single_instance_result.unwrap().unwrap(); // hold the guard until the end of the program
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::default().build())
|
||||
.setup(|app| {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::menu::{MenuBuilder, SubmenuBuilder};
|
||||
let submenu = SubmenuBuilder::new(app, "Edit")
|
||||
.undo()
|
||||
.redo()
|
||||
.copy()
|
||||
.paste()
|
||||
.cut()
|
||||
.select_all()
|
||||
.close_window()
|
||||
.quit()
|
||||
.build()
|
||||
.unwrap();
|
||||
let menu = MenuBuilder::new(app).item(&submenu).build().unwrap();
|
||||
app.set_menu(menu).unwrap();
|
||||
}
|
||||
|
||||
resolve::resolve_setup(app);
|
||||
|
||||
// setup custom scheme
|
||||
let handle = app.handle().clone();
|
||||
// For start new app from schema
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let Some(url) = custom_scheme {
|
||||
log::info!(target: "app", "started with schema");
|
||||
resolve::create_window(&handle.clone());
|
||||
while !is_window_opened() {
|
||||
log::info!(target: "app", "waiting for window open");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Handle::global()
|
||||
.app_handle
|
||||
.lock()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.emit("scheme-request-received", url.clone())
|
||||
.unwrap();
|
||||
}
|
||||
// This operation should terminate the app if app is called by custom scheme and this instance is not the primary instance
|
||||
log_err!(tauri_plugin_deep_link::register(
|
||||
&["clash-nyanpasu", "clash"],
|
||||
move |request| {
|
||||
log::info!(target: "app", "scheme request received: {:?}", &request);
|
||||
resolve::create_window(&handle.clone()); // create window if not exists
|
||||
while !is_window_opened() {
|
||||
log::info!(target: "app", "waiting for window open");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
handle.emit("scheme-request-received", request).unwrap();
|
||||
}
|
||||
));
|
||||
std::thread::spawn(move || {
|
||||
nyanpasu_utils::runtime::block_on(async move {
|
||||
server::run(*server::SERVER_PORT)
|
||||
.await
|
||||
.expect("failed to start server");
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
ipc::get_sys_proxy,
|
||||
ipc::open_app_config_dir,
|
||||
ipc::open_app_data_dir,
|
||||
ipc::open_logs_dir,
|
||||
ipc::open_web_url,
|
||||
ipc::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
ipc::restart_sidecar,
|
||||
ipc::grant_permission,
|
||||
// clash
|
||||
ipc::get_clash_info,
|
||||
ipc::get_clash_logs,
|
||||
ipc::patch_clash_config,
|
||||
ipc::change_clash_core,
|
||||
ipc::get_runtime_config,
|
||||
ipc::get_runtime_yaml,
|
||||
ipc::get_runtime_exists,
|
||||
ipc::get_runtime_logs,
|
||||
ipc::clash_api_get_proxy_delay,
|
||||
ipc::uwp::invoke_uwp_tool,
|
||||
// updater
|
||||
ipc::fetch_latest_core_versions,
|
||||
ipc::update_core,
|
||||
ipc::inspect_updater,
|
||||
ipc::get_core_version,
|
||||
// utils
|
||||
ipc::collect_logs,
|
||||
// verge
|
||||
ipc::get_verge_config,
|
||||
ipc::patch_verge_config,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
ipc::get_profiles,
|
||||
ipc::enhance_profiles,
|
||||
ipc::patch_profiles_config,
|
||||
ipc::view_profile,
|
||||
ipc::patch_profile,
|
||||
ipc::create_profile,
|
||||
ipc::import_profile,
|
||||
ipc::reorder_profile,
|
||||
ipc::update_profile,
|
||||
ipc::delete_profile,
|
||||
ipc::read_profile_file,
|
||||
ipc::save_profile_file,
|
||||
ipc::save_window_size_state,
|
||||
ipc::get_custom_app_dir,
|
||||
ipc::set_custom_app_dir,
|
||||
// service mode
|
||||
ipc::service::status_service,
|
||||
ipc::service::install_service,
|
||||
ipc::service::uninstall_service,
|
||||
ipc::service::start_service,
|
||||
ipc::service::stop_service,
|
||||
ipc::service::restart_service,
|
||||
ipc::is_portable,
|
||||
ipc::get_proxies,
|
||||
ipc::select_proxy,
|
||||
ipc::update_proxy_provider,
|
||||
ipc::restart_application,
|
||||
ipc::collect_envs,
|
||||
ipc::get_server_port,
|
||||
ipc::set_tray_icon,
|
||||
ipc::is_tray_icon_set,
|
||||
ipc::get_core_status,
|
||||
ipc::url_delay_test,
|
||||
ipc::get_ipsb_asn,
|
||||
ipc::open_that,
|
||||
ipc::is_appimage,
|
||||
ipc::get_service_install_prompt,
|
||||
ipc::cleanup_processes,
|
||||
]);
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset();
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
core::tray::on_scale_factor_changed(scale_factor);
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => {
|
||||
log::debug!(target: "app", "window close requested");
|
||||
reset_window_open_counter();
|
||||
let _ = resolve::save_window_state(app_handle, true);
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
crate::utils::dock::macos::hide_dock_icon();
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
log::debug!(target: "app", "window moved or resized");
|
||||
std::thread::sleep(std::time::Duration::from_nanos(1));
|
||||
let _ = resolve::save_window_state(app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,318 +1,5 @@
|
||||
#![feature(auto_traits, negative_impls)]
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod consts;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod ipc;
|
||||
mod server;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
core::handle::Handle,
|
||||
utils::{init, resolve},
|
||||
};
|
||||
use tauri::{api, Manager, SystemTray};
|
||||
use utils::resolve::{is_window_opened, reset_window_open_counter};
|
||||
|
||||
rust_i18n::i18n!("../../locales");
|
||||
|
||||
#[cfg(feature = "deadlock-detection")]
|
||||
fn deadlock_detection() {
|
||||
use parking_lot::deadlock;
|
||||
use std::{thread, time::Duration};
|
||||
use tracing::error;
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
let deadlocks = deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
error!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
error!("Deadlock #{}", i);
|
||||
for t in threads {
|
||||
error!("Thread Id {:#?}", t.thread_id());
|
||||
error!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
// share the tauri async runtime to nyanpasu-utils
|
||||
#[cfg(feature = "deadlock-detection")]
|
||||
deadlock_detection();
|
||||
|
||||
// Should be in first place in order prevent single instance check block everything
|
||||
// Custom scheme check
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
// on macos the plugin handles this (macos doesn't use cli args for the url)
|
||||
let custom_scheme = match std::env::args().nth(1) {
|
||||
Some(url) => url::Url::parse(&url).ok(),
|
||||
None => None,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let custom_scheme: Option<url::Url> = None;
|
||||
|
||||
if custom_scheme.is_none() {
|
||||
// Parse commands
|
||||
cmds::parse().unwrap();
|
||||
};
|
||||
#[cfg(feature = "verge-dev")]
|
||||
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu.dev");
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu");
|
||||
|
||||
// 单例检测
|
||||
let single_instance_result = utils::init::check_singleton();
|
||||
if single_instance_result
|
||||
.as_ref()
|
||||
.is_ok_and(|instance| instance.is_none())
|
||||
{
|
||||
std::process::exit(0);
|
||||
}
|
||||
// Use system locale as default
|
||||
let locale = {
|
||||
let locale = utils::help::get_system_locale();
|
||||
utils::help::mapping_to_i18n_key(&locale)
|
||||
};
|
||||
rust_i18n::set_locale(locale);
|
||||
|
||||
if single_instance_result
|
||||
.as_ref()
|
||||
.is_ok_and(|instance| instance.is_some())
|
||||
{
|
||||
if let Err(e) = init::run_pending_migrations() {
|
||||
utils::dialog::panic_dialog(
|
||||
&format!(
|
||||
"Failed to finish migration event: {}\nYou can see the detailed information at migration.log in your local data dir.\nYou're supposed to submit it as the attachment of new issue.",
|
||||
e,
|
||||
)
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
crate::log_err!(init::init_config());
|
||||
|
||||
// Panic Hook to show a panic dialog and save logs
|
||||
let default_panic = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
error!(format!("panic hook: {:?}", info));
|
||||
utils::dialog::panic_dialog(&format!("{:?}", info));
|
||||
default_panic(info);
|
||||
}));
|
||||
|
||||
let verge = { Config::verge().latest().language.clone().unwrap() };
|
||||
rust_i18n::set_locale(verge.as_str());
|
||||
|
||||
// show a dialog to print the single instance error
|
||||
let _singleton = single_instance_result.unwrap().unwrap(); // hold the guard until the end of the program
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.system_tray(SystemTray::new())
|
||||
.setup(|app| {
|
||||
resolve::resolve_setup(app);
|
||||
// setup custom scheme
|
||||
let handle = app.handle().clone();
|
||||
// For start new app from schema
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if let Some(url) = custom_scheme {
|
||||
log::info!(target: "app", "started with schema");
|
||||
resolve::create_window(&handle.clone());
|
||||
while !is_window_opened() {
|
||||
log::info!(target: "app", "waiting for window open");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
Handle::global()
|
||||
.app_handle
|
||||
.lock()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.emit_all("scheme-request-received", url.clone())
|
||||
.unwrap();
|
||||
}
|
||||
// This operation should terminate the app if app is called by custom scheme and this instance is not the primary instance
|
||||
log_err!(tauri_plugin_deep_link::register(
|
||||
&["clash-nyanpasu", "clash"],
|
||||
move |request| {
|
||||
log::info!(target: "app", "scheme request received: {:?}", &request);
|
||||
resolve::create_window(&handle.clone()); // create window if not exists
|
||||
while !is_window_opened() {
|
||||
log::info!(target: "app", "waiting for window open");
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
handle.emit_all("scheme-request-received", request).unwrap();
|
||||
}
|
||||
));
|
||||
std::thread::spawn(move || {
|
||||
nyanpasu_utils::runtime::block_on(async move {
|
||||
server::run(*server::SERVER_PORT)
|
||||
.await
|
||||
.expect("failed to start server");
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
ipc::get_sys_proxy,
|
||||
ipc::open_app_config_dir,
|
||||
ipc::open_app_data_dir,
|
||||
ipc::open_logs_dir,
|
||||
ipc::open_web_url,
|
||||
ipc::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
ipc::restart_sidecar,
|
||||
ipc::grant_permission,
|
||||
// clash
|
||||
ipc::get_clash_info,
|
||||
ipc::get_clash_logs,
|
||||
ipc::patch_clash_config,
|
||||
ipc::change_clash_core,
|
||||
ipc::get_runtime_config,
|
||||
ipc::get_runtime_yaml,
|
||||
ipc::get_runtime_exists,
|
||||
ipc::get_runtime_logs,
|
||||
ipc::clash_api_get_proxy_delay,
|
||||
ipc::uwp::invoke_uwp_tool,
|
||||
// updater
|
||||
ipc::fetch_latest_core_versions,
|
||||
ipc::update_core,
|
||||
ipc::inspect_updater,
|
||||
ipc::get_core_version,
|
||||
// utils
|
||||
ipc::collect_logs,
|
||||
// verge
|
||||
ipc::get_verge_config,
|
||||
ipc::patch_verge_config,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
ipc::get_profiles,
|
||||
ipc::enhance_profiles,
|
||||
ipc::patch_profiles_config,
|
||||
ipc::view_profile,
|
||||
ipc::patch_profile,
|
||||
ipc::create_profile,
|
||||
ipc::import_profile,
|
||||
ipc::reorder_profile,
|
||||
ipc::update_profile,
|
||||
ipc::delete_profile,
|
||||
ipc::read_profile_file,
|
||||
ipc::save_profile_file,
|
||||
ipc::save_window_size_state,
|
||||
ipc::get_custom_app_dir,
|
||||
ipc::set_custom_app_dir,
|
||||
// service mode
|
||||
ipc::service::status_service,
|
||||
ipc::service::install_service,
|
||||
ipc::service::uninstall_service,
|
||||
ipc::service::start_service,
|
||||
ipc::service::stop_service,
|
||||
ipc::service::restart_service,
|
||||
ipc::is_portable,
|
||||
ipc::get_proxies,
|
||||
ipc::select_proxy,
|
||||
ipc::update_proxy_provider,
|
||||
ipc::restart_application,
|
||||
ipc::collect_envs,
|
||||
ipc::get_server_port,
|
||||
ipc::set_tray_icon,
|
||||
ipc::is_tray_icon_set,
|
||||
ipc::get_core_status,
|
||||
ipc::url_delay_test,
|
||||
ipc::get_ipsb_asn,
|
||||
ipc::open_that,
|
||||
ipc::is_appimage,
|
||||
ipc::get_service_install_prompt,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::{Menu, MenuItem, Submenu};
|
||||
|
||||
builder = builder.menu(
|
||||
Menu::new().add_submenu(Submenu::new(
|
||||
"Edit",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::SelectAll)
|
||||
.add_native_item(MenuItem::CloseWindow)
|
||||
.add_native_item(MenuItem::Quit),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
}
|
||||
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
|
||||
resolve::resolve_reset();
|
||||
nyanpasu_utils::runtime::block_on(async {
|
||||
if let Err(e) = crate::core::CoreManager::global().stop_core().await {
|
||||
log::error!(target: "app", "failed to stop core while dispatch updater: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
core::tray::on_scale_factor_changed(scale_factor);
|
||||
}
|
||||
tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => {
|
||||
// log::info!(target: "app", "window close requested");
|
||||
reset_window_open_counter();
|
||||
let _ = resolve::save_window_state(app_handle, true);
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe {
|
||||
crate::utils::dock::macos::hide_dock_icon();
|
||||
}
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
// log::info!(target: "app", "window moved or resized");
|
||||
std::thread::sleep(std::time::Duration::from_nanos(1));
|
||||
let _ = resolve::save_window_state(app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
fn main() {
|
||||
app_lib::run().unwrap();
|
||||
}
|
||||
|
@@ -60,8 +60,11 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
|
||||
let mut core = HashMap::new();
|
||||
for c in CoreType::get_supported_cores() {
|
||||
let name: &str = c.as_ref();
|
||||
let command = tauri::api::process::Command::new_sidecar(name)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let mut command = std::process::Command::new(
|
||||
super::dirs::get_data_or_sidecar_path(name)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?,
|
||||
);
|
||||
let output = command
|
||||
.args(if matches!(c, CoreType::Clash(ClashCoreType::ClashRust)) {
|
||||
["-V"]
|
||||
@@ -70,9 +73,10 @@ pub fn collect_envs<'a>() -> Result<EnvInfo<'a>, std::io::Error> {
|
||||
})
|
||||
.output()
|
||||
.expect("failed to execute sidecar command");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
core.insert(
|
||||
Cow::Borrowed(name),
|
||||
Cow::Owned(output.stdout.replace("\n\n", " ").trim().to_owned()),
|
||||
Cow::Owned(stdout.replace("\n\n", " ").trim().to_owned()),
|
||||
);
|
||||
}
|
||||
Ok(EnvInfo {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use rfd::{MessageButtons, MessageDialog, MessageLevel};
|
||||
use rfd::{MessageButtons, MessageDialog, MessageDialogResult, MessageLevel};
|
||||
use rust_i18n::t;
|
||||
|
||||
pub fn panic_dialog(msg: &str) {
|
||||
@@ -12,12 +12,15 @@ pub fn panic_dialog(msg: &str) {
|
||||
}
|
||||
|
||||
pub fn migrate_dialog(msg: &str) -> bool {
|
||||
MessageDialog::new()
|
||||
.set_level(MessageLevel::Warning)
|
||||
.set_title("Clash Nyanpasu Migration")
|
||||
.set_buttons(MessageButtons::YesNo)
|
||||
.set_description(msg)
|
||||
.show()
|
||||
matches!(
|
||||
MessageDialog::new()
|
||||
.set_level(MessageLevel::Warning)
|
||||
.set_title("Clash Nyanpasu Migration")
|
||||
.set_buttons(MessageButtons::YesNo)
|
||||
.set_description(msg)
|
||||
.show(),
|
||||
MessageDialogResult::Yes
|
||||
)
|
||||
}
|
||||
|
||||
pub fn error_dialog(msg: String) {
|
||||
|
@@ -3,10 +3,7 @@ use anyhow::Result;
|
||||
use nyanpasu_utils::dirs::{suggest_config_dir, suggest_data_dir};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{borrow::Cow, fs, path::PathBuf};
|
||||
use tauri::{
|
||||
api::path::{home_dir, resource_dir},
|
||||
Env,
|
||||
};
|
||||
use tauri::{utils::platform::resource_dir, Env};
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const PREVIOUS_APP_NAME: &str = "clash-verge";
|
||||
@@ -109,7 +106,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if !get_portable_flag() {
|
||||
Ok(home_dir()
|
||||
Ok(dirs::home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to check old app home dir"))?
|
||||
.join(".config")
|
||||
.join(PREVIOUS_APP_NAME))
|
||||
@@ -120,7 +117,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Ok(home_dir()
|
||||
Ok(dirs::home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
||||
.join(".config")
|
||||
.join(PREVIOUS_APP_NAME))
|
||||
@@ -133,7 +130,7 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
|
||||
)]
|
||||
pub fn app_home_dir() -> Result<PathBuf> {
|
||||
if cfg!(feature = "verge-dev") {
|
||||
return Ok(home_dir()
|
||||
return Ok(dirs::home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
||||
.join(".config")
|
||||
.join(APP_NAME));
|
||||
@@ -147,7 +144,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
if let Some(reg_app_dir) = reg_app_dir {
|
||||
return Ok(reg_app_dir);
|
||||
}
|
||||
return Ok(home_dir()
|
||||
return Ok(dirs::home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||
.join(".config")
|
||||
.join(APP_NAME));
|
||||
@@ -156,7 +153,7 @@ pub fn app_home_dir() -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Ok(home_dir()
|
||||
Ok(dirs::home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
||||
.join(".config")
|
||||
.join(APP_NAME))
|
||||
@@ -168,7 +165,7 @@ pub fn app_resources_dir() -> Result<PathBuf> {
|
||||
let app_handle = handle.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
let res_dir = resource_dir(app_handle.package_info(), &Env::default())
|
||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||
.map_err(|_| anyhow::anyhow!("failed to get the resource dir"))?
|
||||
.join("resources");
|
||||
return Ok(res_dir);
|
||||
};
|
||||
@@ -315,6 +312,28 @@ fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_data_or_sidecar_path(binary_name: impl AsRef<str>) -> Result<PathBuf> {
|
||||
let binary_name = binary_name.as_ref();
|
||||
let data_dir = app_data_dir()?;
|
||||
let path = data_dir.join(if cfg!(windows) && !binary_name.ends_with(".exe") {
|
||||
format!("{}.exe", binary_name)
|
||||
} else {
|
||||
binary_name.to_string()
|
||||
});
|
||||
if path.exists() {
|
||||
return Ok(data_dir);
|
||||
}
|
||||
|
||||
let install_dir = app_install_dir()?;
|
||||
let path = install_dir.join(if cfg!(windows) && !binary_name.ends_with(".exe") {
|
||||
format!("{}.exe", binary_name)
|
||||
} else {
|
||||
binary_name.to_string()
|
||||
});
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
mod test {
|
||||
#[test]
|
||||
fn test_dir_placeholder() {
|
||||
|
@@ -15,16 +15,13 @@ use std::{
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
};
|
||||
use tauri::{
|
||||
api::{
|
||||
process::current_binary,
|
||||
shell::{open, Program},
|
||||
},
|
||||
AppHandle, Manager,
|
||||
};
|
||||
use tauri::{process::current_binary, AppHandle, Manager};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tracing::{debug, warn};
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
use crate::trace_err;
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
if !path.exists() {
|
||||
@@ -105,20 +102,27 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let code = "code";
|
||||
|
||||
let _ = match Program::from_str(code) {
|
||||
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Can't find VScode `{err}`");
|
||||
// default open
|
||||
open(&app.shell_scope(), path.to_string_lossy(), None)
|
||||
}
|
||||
};
|
||||
let shell = app.shell();
|
||||
|
||||
trace_err!(
|
||||
match which::which(code) {
|
||||
Ok(_) => crate::utils::open::with(path, code),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "Can't find VScode `{err}`");
|
||||
// default open
|
||||
shell
|
||||
.open(path.to_string_lossy().to_string(), None)
|
||||
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
|
||||
}
|
||||
},
|
||||
"Can't open file"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_system_locale() -> String {
|
||||
tauri::api::os::locale().unwrap_or("en-US".to_string())
|
||||
tauri_plugin_os::locale().unwrap_or("en-US".to_string())
|
||||
}
|
||||
|
||||
pub fn mapping_to_i18n_key(locale_key: &str) -> &'static str {
|
||||
@@ -215,10 +219,12 @@ pub fn get_max_scale_factor() -> f64 {
|
||||
}
|
||||
|
||||
#[instrument(skip(app_handle))]
|
||||
fn cleanup_processes(app_handle: &AppHandle) {
|
||||
pub fn cleanup_processes(app_handle: &AppHandle) {
|
||||
let _ = super::resolve::save_window_state(app_handle, true);
|
||||
super::resolve::resolve_reset();
|
||||
tauri::api::process::kill_children();
|
||||
let _ = nyanpasu_utils::runtime::block_on(async move {
|
||||
crate::core::CoreManager::global().stop_core().await
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(skip(app_handle))]
|
||||
@@ -263,9 +269,9 @@ macro_rules! log_err {
|
||||
}
|
||||
};
|
||||
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(_) = $result {
|
||||
log::error!(target: "app", "{}", $err_str);
|
||||
($result: expr, $label: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!(target: "app", "{}: {:#?}", $label, err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -12,3 +12,16 @@ pub fn that<T: AsRef<OsStr>>(path: T) -> std::io::Result<()> {
|
||||
open::that(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with<T: AsRef<OsStr>>(path: T, program: &str) -> std::io::Result<()> {
|
||||
// A dirty workaround for AppImage
|
||||
if std::env::var("APPIMAGE").is_ok() {
|
||||
std::process::Command::new(program)
|
||||
.arg(path)
|
||||
.env_remove("LD_LIBRARY_PATH")
|
||||
.status()?;
|
||||
Ok(())
|
||||
} else {
|
||||
open::with(path, program)
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,8 @@ use std::{
|
||||
net::TcpListener,
|
||||
sync::atomic::{AtomicU16, Ordering},
|
||||
};
|
||||
use tauri::{api::process::Command, async_runtime::block_on, App, AppHandle, Manager};
|
||||
use tauri::{async_runtime::block_on, App, AppHandle, Emitter, Listener, Manager};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
static OPEN_WINDOWS_COUNTER: AtomicU16 = AtomicU16::new(0);
|
||||
|
||||
@@ -85,7 +86,7 @@ pub fn find_unused_port() -> Result<u16> {
|
||||
pub fn resolve_setup(app: &mut App) {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
app.listen_global("react_app_mounted", move |_| {
|
||||
app.listen("react_app_mounted", move |_| {
|
||||
tracing::debug!("Frontend React App is mounted, reset open window counter");
|
||||
reset_window_open_counter();
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -94,7 +95,7 @@ pub fn resolve_setup(app: &mut App) {
|
||||
}
|
||||
});
|
||||
|
||||
handle::Handle::global().init(app.app_handle());
|
||||
handle::Handle::global().init(app.app_handle().clone());
|
||||
|
||||
log_err!(init::init_resources());
|
||||
log_err!(init::init_service());
|
||||
@@ -134,20 +135,31 @@ pub fn resolve_setup(app: &mut App) {
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
log::trace!("init system tray");
|
||||
#[cfg(windows)]
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor
|
||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||
let app_handle = app.app_handle().clone();
|
||||
app.listen("update_systray", move |_| {
|
||||
// Fix the GTK should run on main thread issue
|
||||
let app_handle_clone = app_handle.clone();
|
||||
log_err!(app_handle.run_on_main_thread(move || {
|
||||
log_err!(
|
||||
tray::Tray::update_systray(&app_handle_clone),
|
||||
"failed to update systray"
|
||||
);
|
||||
}));
|
||||
});
|
||||
log_err!(app.emit("update_systray", ()));
|
||||
|
||||
let silent_start = { Config::verge().data().enable_silent_start };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window(&app.app_handle());
|
||||
create_window(app.app_handle());
|
||||
}
|
||||
|
||||
log_err!(sysopt::Sysopt::global().init_launch());
|
||||
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||
log_err!(hotkey::Hotkey::global().init(app.app_handle().clone()));
|
||||
|
||||
// setup jobs
|
||||
log_err!(JobsManager::global_register());
|
||||
@@ -165,8 +177,10 @@ pub fn resolve_reset() {
|
||||
}
|
||||
|
||||
/// create main window
|
||||
#[tracing_attributes::instrument(skip(app_handle))]
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
tracing::debug!("main window is already opened, try to show it");
|
||||
if OPEN_WINDOWS_COUNTER.load(Ordering::Acquire) == 0 {
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
@@ -183,10 +197,11 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
.unwrap_or(&false)
|
||||
};
|
||||
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
tracing::debug!("create main window...");
|
||||
let mut builder = tauri::WebviewWindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WindowUrl::App("/".into()),
|
||||
tauri::WebviewUrl::App("/".into()),
|
||||
)
|
||||
.title("Clash Nyanpasu")
|
||||
.fullscreen(false)
|
||||
@@ -221,6 +236,7 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
|
||||
.build();
|
||||
#[cfg(target_os = "macos")]
|
||||
let win_res = builder
|
||||
@@ -229,11 +245,15 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.build();
|
||||
#[cfg(target_os = "linux")]
|
||||
let win_res = builder.decorations(true).transparent(true).build();
|
||||
let win_res = builder.decorations(true).transparent(false).build();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_controls_and_log_error(app_handle: &tauri::AppHandle, window_name: &str) {
|
||||
match app_handle.get_window(window_name).unwrap().ns_window() {
|
||||
match app_handle
|
||||
.get_webview_window(window_name)
|
||||
.unwrap()
|
||||
.ns_window()
|
||||
{
|
||||
Ok(raw_window) => {
|
||||
let window_id: cocoa::base::id = raw_window as _;
|
||||
set_window_controls_pos(window_id, 26.0, 26.0);
|
||||
@@ -247,8 +267,6 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
match win_res {
|
||||
Ok(win) => {
|
||||
use tauri::{PhysicalPosition, PhysicalSize};
|
||||
#[cfg(windows)]
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
if win_state.is_some() {
|
||||
let state = win_state.as_ref().unwrap();
|
||||
@@ -273,7 +291,7 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
trace_err!(set_shadow(&win, true), "set win shadow");
|
||||
trace_err!(win.set_shadow(true), "set win shadow");
|
||||
log::trace!("try to calculate the monitor size");
|
||||
let center = (|| -> Result<bool> {
|
||||
let center;
|
||||
@@ -310,7 +328,9 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
win.open_devtools();
|
||||
if let Some(webview_window) = win.get_webview_window("main") {
|
||||
webview_window.open_devtools();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -329,17 +349,23 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "failed to create window, {err}");
|
||||
if let Some(win) = app_handle.get_webview_window("main") {
|
||||
// Cleanup window if failed to create, it's a workaround for tauri bug
|
||||
log_err!(
|
||||
win.destroy(),
|
||||
"occur error when close window while failed to create"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use webview2_com_bridge::{
|
||||
webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6,
|
||||
windows::core::Interface,
|
||||
};
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings6;
|
||||
use windows_core::Interface;
|
||||
|
||||
app_handle
|
||||
.get_window("main")
|
||||
.get_webview_window("main")
|
||||
.unwrap()
|
||||
.with_webview(|webview| unsafe {
|
||||
let settings = webview
|
||||
@@ -358,7 +384,7 @@ pub fn create_window(app_handle: &AppHandle) {
|
||||
|
||||
/// close main window
|
||||
pub fn close_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
trace_err!(window.close(), "close window");
|
||||
reset_window_open_counter()
|
||||
}
|
||||
@@ -366,12 +392,12 @@ pub fn close_window(app_handle: &AppHandle) {
|
||||
|
||||
/// is window open
|
||||
pub fn is_window_open(app_handle: &AppHandle) -> bool {
|
||||
app_handle.get_window("main").is_some()
|
||||
app_handle.get_webview_window("main").is_some()
|
||||
}
|
||||
|
||||
pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
||||
let win = app_handle
|
||||
.get_window("main")
|
||||
.get_webview_window("main")
|
||||
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||
let current_monitor = win.current_monitor()?;
|
||||
let verge = Config::verge();
|
||||
@@ -412,21 +438,23 @@ pub fn save_window_state(app_handle: &AppHandle, save_to_file: bool) -> Result<(
|
||||
|
||||
/// resolve core version
|
||||
// TODO: use enum instead
|
||||
pub fn resolve_core_version(core_type: &ClashCore) -> Result<String> {
|
||||
pub async fn resolve_core_version(app_handle: &AppHandle, core_type: &ClashCore) -> Result<String> {
|
||||
let shell = app_handle.shell();
|
||||
let core = core_type.clone().to_string();
|
||||
log::debug!(target: "app", "check config in `{core}`");
|
||||
let cmd = match core_type {
|
||||
ClashCore::ClashPremium | ClashCore::Mihomo | ClashCore::MihomoAlpha => {
|
||||
Command::new_sidecar(core)?.args(["-v"])
|
||||
shell.sidecar(core)?.args(["-v"])
|
||||
}
|
||||
ClashCore::ClashRs => Command::new_sidecar(core)?.args(["-V"]),
|
||||
ClashCore::ClashRs => shell.sidecar(core)?.args(["-V"]),
|
||||
};
|
||||
let out = cmd.output()?;
|
||||
log::debug!(target: "app", "get core version: {:?}", out);
|
||||
let out = cmd.output().await?;
|
||||
if !out.status.success() {
|
||||
return Err(anyhow::anyhow!("failed to get core version"));
|
||||
}
|
||||
let out = out.stdout.trim().split(' ').collect::<Vec<&str>>();
|
||||
let out = String::from_utf8_lossy(&out.stdout);
|
||||
log::trace!(target: "app", "get core version: {:?}", out);
|
||||
let out = out.trim().split(' ').collect::<Vec<&str>>();
|
||||
for item in out {
|
||||
log::debug!(target: "app", "check item: {}", item);
|
||||
if item.starts_with('v')
|
||||
|
@@ -1,118 +1,86 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Nyanpasu",
|
||||
"version": "1.6.1"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "./tmp/dist",
|
||||
"devPath": "http://localhost:3000/",
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)"
|
||||
},
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/win-tray-icon.png",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "moe.elaina.clash.nyanpasu",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"externalBin": [
|
||||
"sidecar/clash",
|
||||
"sidecar/mihomo",
|
||||
"sidecar/mihomo-alpha",
|
||||
"sidecar/clash-rs",
|
||||
"sidecar/nyanpasu-service"
|
||||
],
|
||||
"copyright": "© 2024 Clash Nyanpasu All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
|
||||
"longDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
|
||||
"deb": {
|
||||
"depends": ["openssl"]
|
||||
"$schema": "../../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"webviewInstallMode": {
|
||||
"silent": true,
|
||||
"type": "embedBootstrapper"
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "11.0",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US", "ru-RU"],
|
||||
"template": "./templates/installer.wxs",
|
||||
"fragmentPaths": ["./templates/cleanup.wxs"]
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"webviewInstallMode": {
|
||||
"silent": true,
|
||||
"type": "embedBootstrapper"
|
||||
},
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US", "ru-RU"],
|
||||
"template": "./templates/installer.wxs",
|
||||
"license": "../../LICENSE",
|
||||
"fragmentPaths": ["./templates/cleanup.wxs"]
|
||||
},
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
"installerIcon": "icons/icon.ico",
|
||||
"languages": ["SimpChinese", "English", "Russian"],
|
||||
"license": "../../LICENSE",
|
||||
"installMode": "both",
|
||||
"template": "./templates/installer.nsi"
|
||||
}
|
||||
"nsis": {
|
||||
"displayLanguageSelector": true,
|
||||
"installerIcon": "icons/icon.ico",
|
||||
"languages": ["SimpChinese", "English", "Russian"],
|
||||
"template": "./templates/installer.nsi",
|
||||
"installMode": "both"
|
||||
}
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"externalBin": [
|
||||
"sidecar/clash",
|
||||
"sidecar/mihomo",
|
||||
"sidecar/mihomo-alpha",
|
||||
"sidecar/clash-rs",
|
||||
"sidecar/nyanpasu-service"
|
||||
],
|
||||
"copyright": "© 2024 Clash Nyanpasu All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
|
||||
"longDescription": "Clash Nyanpasu! (∠・ω< )⌒☆",
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "11.0",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["openssl"]
|
||||
}
|
||||
},
|
||||
"licenseFile": "../../LICENSE",
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "pnpm run-p web:build generate:git-info && echo $(pwd)",
|
||||
"frontendDist": "./tmp/dist",
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"devUrl": "http://localhost:3000/"
|
||||
},
|
||||
"productName": "Clash Nyanpasu",
|
||||
"version": "1.6.0",
|
||||
"identifier": "moe.elaina.clash.nyanpasu",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK",
|
||||
"endpoints": [
|
||||
"https://mirror.ghproxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json",
|
||||
"https://gh-proxy.com/https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/LibNyanpasu/clash-nyanpasu/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK"
|
||||
},
|
||||
"allowlist": {
|
||||
"shell": {
|
||||
"all": true
|
||||
},
|
||||
"window": {
|
||||
"all": true
|
||||
},
|
||||
"process": {
|
||||
"all": true
|
||||
},
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"os": {
|
||||
"all": true
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"fs": {
|
||||
"all": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' data: asset: blob: http://localhost:* https:;"
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' data: asset: blob: http://localhost:* https:; connect-src ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:* wss://*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
clash-nyanpasu/backend/tauri/tauri.windows.conf.json
Normal file
6
clash-nyanpasu/backend/tauri/tauri.windows.conf.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"targets": ["nsis"]
|
||||
}
|
||||
}
|
@@ -481,11 +481,12 @@ FunctionEnd
|
||||
|
||||
!macro CheckAllNyanpasuProcesses
|
||||
!insertmacro CheckNyanpasuProcess "Clash Nyanpasu.exe" "1"
|
||||
; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "2"
|
||||
!insertmacro CheckNyanpasuProcess "clash.exe" "3"
|
||||
!insertmacro CheckNyanpasuProcess "clash-rs.exe" "4"
|
||||
!insertmacro CheckNyanpasuProcess "mihomo.exe" "5"
|
||||
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "6"
|
||||
!insertmacro CheckNyanpasuProcess "clash-nyanpasu.exe" "2"
|
||||
; !insertmacro CheckNyanpasuProcess "clash-verge-service.exe" "3"
|
||||
!insertmacro CheckNyanpasuProcess "clash.exe" "4"
|
||||
!insertmacro CheckNyanpasuProcess "clash-rs.exe" "5"
|
||||
!insertmacro CheckNyanpasuProcess "mihomo.exe" "6"
|
||||
!insertmacro CheckNyanpasuProcess "mihomo-alpha.exe" "7"
|
||||
!macroend
|
||||
|
||||
; Section CheckProcesses
|
||||
|
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "webview2-com-bridge"
|
||||
version = "0.1.0"
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
webview2-com = "0.19"
|
||||
windows = "0.39"
|
@@ -1,2 +0,0 @@
|
||||
pub use webview2_com;
|
||||
pub use windows;
|
@@ -11,7 +11,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "1.6.0",
|
||||
"@tauri-apps/api": "2.0.0-rc.4",
|
||||
"ahooks": "3.8.1",
|
||||
"ofetch": "1.3.4",
|
||||
"react": "18.3.1",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { IPSBResponse } from "@/openapi";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ManifestVersion } from "./core";
|
||||
import {
|
||||
ClashConfig,
|
||||
@@ -259,3 +259,7 @@ export const isAppImage = async () => {
|
||||
export const getServiceInstallPrompt = async () => {
|
||||
return await invoke<string>("get_service_install_prompt");
|
||||
};
|
||||
|
||||
export const cleanupProcesses = async () => {
|
||||
return await invoke<void>("cleanup_processes");
|
||||
};
|
||||
|
@@ -22,7 +22,7 @@
|
||||
"@mui/material": "5.16.7",
|
||||
"@nyanpasu/interface": "workspace:^",
|
||||
"@nyanpasu/ui": "workspace:^",
|
||||
"@tauri-apps/api": "1.6.0",
|
||||
"@tauri-apps/api": "2.0.0-rc.4",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"ahooks": "3.8.1",
|
||||
"allotment": "1.20.2",
|
||||
@@ -52,13 +52,21 @@
|
||||
"@csstools/normalize.css": "12.1.1",
|
||||
"@emotion/babel-plugin": "11.12.0",
|
||||
"@emotion/react": "11.13.3",
|
||||
"@iconify/json": "2.2.245",
|
||||
"@iconify/json": "2.2.246",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-fs": "2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-notification": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-rc.2",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@vitejs/plugin-react": "4.3.1",
|
||||
"@vitejs/plugin-react-swc": "3.7.0",
|
||||
"clsx": "2.1.1",
|
||||
"meta-json-schema": "libnyanpasu/meta-json-schema#main",
|
||||
"meta-json-schema": "1.18.8",
|
||||
"monaco-yaml": "5.2.2",
|
||||
"nanoid": "5.0.7",
|
||||
"sass": "1.78.0",
|
||||
|
@@ -2,7 +2,7 @@ import { Allotment } from "allotment";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { alpha, useTheme } from "@mui/material";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import "allotment/dist/style.css";
|
||||
import { ReactNode } from "react";
|
||||
import { LayoutControl } from "../layout/layout-control";
|
||||
@@ -10,6 +10,8 @@ import styles from "./app-container.module.scss";
|
||||
import AppDrawer from "./app-drawer";
|
||||
import DrawerContent from "./drawer-content";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
export const AppContainer = ({
|
||||
|
@@ -81,7 +81,7 @@ export const ProxyShortcuts = () => {
|
||||
} catch (e) {
|
||||
message(`Activation failed!`, {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useMemoizedFn } from "ahooks";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { notification, NotificationType } from "@/utils/notification";
|
||||
import {
|
||||
CloseRounded,
|
||||
@@ -13,8 +13,10 @@ import {
|
||||
import { alpha, Button, ButtonProps, useTheme } from "@mui/material";
|
||||
import { save_window_size_state, useNyanpasu } from "@nyanpasu/interface";
|
||||
import { cn } from "@nyanpasu/ui";
|
||||
import { platform, type Platform } from "@tauri-apps/api/os";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { platform as getPlatform } from "@tauri-apps/plugin-os";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
|
||||
const CtrlButton = (props: ButtonProps) => {
|
||||
const { palette } = useTheme();
|
||||
@@ -35,7 +37,7 @@ export const LayoutControl = ({ className }: { className?: string }) => {
|
||||
const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu();
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
|
||||
const [platfrom, setPlatform] = useState<Platform>("win32");
|
||||
const platform = useRef(getPlatform());
|
||||
|
||||
const updateMaximized = async () => {
|
||||
try {
|
||||
@@ -60,11 +62,6 @@ export const LayoutControl = ({ className }: { className?: string }) => {
|
||||
// Update the maximized state
|
||||
updateMaximized();
|
||||
|
||||
// Get the platform
|
||||
platform().then((platform) => {
|
||||
setPlatform(() => platform);
|
||||
});
|
||||
|
||||
// Add a resize handler to update the maximized state
|
||||
const resizeHandler = debounce(updateMaximized, 1000);
|
||||
|
||||
@@ -112,7 +109,7 @@ export const LayoutControl = ({ className }: { className?: string }) => {
|
||||
|
||||
<CtrlButton
|
||||
onClick={() => {
|
||||
if (platfrom === "win32") {
|
||||
if (platform.current === "windows") {
|
||||
save_window_size_state().finally(() => {
|
||||
appWindow.close();
|
||||
});
|
||||
|
@@ -7,7 +7,9 @@ import { themeMode as themeModeAtom } from "@/store";
|
||||
import { alpha, darken, lighten, Theme, useColorScheme } from "@mui/material";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { cn, createMDYTheme } from "@nyanpasu/ui";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
|
||||
const applyRootStyleVar = (mode: "light" | "dark", theme: Theme) => {
|
||||
const root = document.documentElement;
|
||||
|
@@ -54,7 +54,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
}
|
||||
} catch (e) {
|
||||
message(`Apply error: ${formatError(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ export const ProfileDialog = ({
|
||||
const onSubmit = handleSubmit(async (form) => {
|
||||
if (editorHasError()) {
|
||||
message("Please fix the error before saving", {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@@ -122,7 +122,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
: `Error setting profile: \n ${err instanceof Error ? err.message : String(err)}`,
|
||||
{
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
@@ -161,7 +161,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
} catch (err) {
|
||||
message(`Delete failed: \n ${JSON.stringify(err)}`, {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useClash } from "@nyanpasu/interface";
|
||||
import { readText } from "@tauri-apps/api/clipboard";
|
||||
import { readText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
|
||||
export const QuickImport = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { readTextFile } from "@tauri-apps/api/fs";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { readTextFile } from "@tauri-apps/plugin-fs";
|
||||
|
||||
const isWin = getSystem() === "windows";
|
||||
|
||||
|
@@ -103,7 +103,7 @@ export const ScriptDialog = ({
|
||||
const onSubmit = form.handleSubmit(async (data) => {
|
||||
if (editorHasError()) {
|
||||
message("Please fix the error before submitting", {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ export const ProxiesProvider = ({ provider }: ProxiesProviderProps) => {
|
||||
await updateProxiesProviders(provider.name);
|
||||
} catch (e) {
|
||||
message(`Update ${provider.name} failed.\n${String(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
} finally {
|
||||
|
@@ -26,7 +26,7 @@ export default function RulesProvider({ provider }: RulesProviderProps) {
|
||||
await updateRulesProviders(provider.name);
|
||||
} catch (e) {
|
||||
message(`Update ${provider.name} failed.\n${String(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
} finally {
|
||||
|
@@ -16,7 +16,7 @@ export const UpdateProviders = () => {
|
||||
const handleProviderUpdate = useLockFn(async () => {
|
||||
if (!getRulesProviders.data) {
|
||||
message(`No Providers.`, {
|
||||
type: "info",
|
||||
kind: "info",
|
||||
title: t("Info"),
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export const UpdateProviders = () => {
|
||||
);
|
||||
} catch (e) {
|
||||
message(`Update all failed.\n${String(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
} finally {
|
||||
|
@@ -16,7 +16,7 @@ export const UpdateProxiesProviders = () => {
|
||||
const handleProviderUpdate = useLockFn(async () => {
|
||||
if (!getProxiesProviders.data) {
|
||||
message(`No Providers.`, {
|
||||
type: "info",
|
||||
kind: "info",
|
||||
title: t("Info"),
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export const UpdateProxiesProviders = () => {
|
||||
);
|
||||
} catch (e) {
|
||||
message(`Update all failed.\n${String(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
} finally {
|
||||
|
@@ -6,6 +6,7 @@ import ClashRs from "@/assets/image/core/clash-rs.png";
|
||||
import ClashMeta from "@/assets/image/core/clash.meta.png";
|
||||
import Clash from "@/assets/image/core/clash.png";
|
||||
import { formatError } from "@/utils";
|
||||
import { message } from "@/utils/notification";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import FiberManualRecord from "@mui/icons-material/FiberManualRecord";
|
||||
import Update from "@mui/icons-material/Update";
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
useNyanpasu,
|
||||
} from "@nyanpasu/interface";
|
||||
import { cleanDeepClickEvent, cn } from "@nyanpasu/ui";
|
||||
import { message } from "@tauri-apps/api/dialog";
|
||||
|
||||
export const getImage = (core: ClashCore) => {
|
||||
switch (core) {
|
||||
@@ -175,12 +175,12 @@ export const ClashCoreItem = ({
|
||||
getClashCore.mutate();
|
||||
|
||||
message(`Successfully update core ${data.name}`, {
|
||||
type: "info",
|
||||
kind: "info",
|
||||
title: t("Success"),
|
||||
});
|
||||
} catch (e) {
|
||||
message(`Update failed. ${formatError(e)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
} finally {
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
setTrayIcon as setTrayIconCall,
|
||||
} from "@nyanpasu/interface";
|
||||
import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -53,7 +53,7 @@ function TrayIconItem({ mode }: { mode: "system_proxy" | "tun" | "normal" }) {
|
||||
return await setTrayIconCall(mode, selected);
|
||||
} catch (e) {
|
||||
message(formatError(e), {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
} finally {
|
||||
setTs(Date.now());
|
||||
|
@@ -24,7 +24,7 @@ export const SettingClashBase = () => {
|
||||
} catch (e) {
|
||||
message(`Failed to Open UWP Tools.\n${JSON.stringify(e)}`, {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -27,7 +27,7 @@ export const SettingClashCore = () => {
|
||||
} = useNyanpasu({
|
||||
onLatestCoreError: (error) => {
|
||||
message(`Fetch latest core failed: ${formatError(error)}`, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
},
|
||||
@@ -57,7 +57,7 @@ export const SettingClashCore = () => {
|
||||
await setClashCore(core);
|
||||
|
||||
message(`Successfully switch to ${core}`, {
|
||||
type: "info",
|
||||
kind: "info",
|
||||
title: t("Success"),
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -66,7 +66,7 @@ export const SettingClashCore = () => {
|
||||
e instanceof Error ? e.message : String(e)
|
||||
}`,
|
||||
{
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
},
|
||||
);
|
||||
@@ -80,14 +80,18 @@ export const SettingClashCore = () => {
|
||||
await restartSidecar();
|
||||
|
||||
message(t("Successfully restart core"), {
|
||||
type: "info",
|
||||
kind: "info",
|
||||
title: t("Success"),
|
||||
});
|
||||
} catch (e) {
|
||||
message("Restart failed, please check log.", {
|
||||
type: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
message(
|
||||
"Restart failed, full detailed please check the log.\n\nError:" +
|
||||
formatError(e),
|
||||
{
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,7 +100,7 @@ export const SettingClashCore = () => {
|
||||
await getLatestCore.mutate();
|
||||
} catch (e) {
|
||||
message("Fetch failed, please check your internet connection.", {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
}
|
||||
|
@@ -83,7 +83,7 @@ export const SettingClashExternal = () => {
|
||||
} catch (e) {
|
||||
message(JSON.stringify(e), {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
} finally {
|
||||
setExpand(false);
|
||||
|
@@ -45,12 +45,12 @@ export const SettingClashPort = () => {
|
||||
} catch (e) {
|
||||
message(JSON.stringify(e), {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
} finally {
|
||||
message(t("After restart to take effect"), {
|
||||
title: t("Success"),
|
||||
type: "info",
|
||||
kind: "info",
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
setCustomAppDir,
|
||||
} from "@nyanpasu/interface";
|
||||
import { BaseCard } from "@nyanpasu/ui";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { PaperButton } from "./modules/nyanpasu-path";
|
||||
|
||||
export const SettingNyanpasuPath = () => {
|
||||
@@ -36,7 +36,7 @@ export const SettingNyanpasuPath = () => {
|
||||
if (Array.isArray(selected)) {
|
||||
message(t("Multiple directories are not supported"), {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -46,7 +46,7 @@ export const SettingNyanpasuPath = () => {
|
||||
|
||||
message(t("App directory changed successfully"), {
|
||||
title: t("Success"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
|
||||
await sleep(1000);
|
||||
@@ -55,7 +55,7 @@ export const SettingNyanpasuPath = () => {
|
||||
} catch (e) {
|
||||
message(`Migration failed! ${JSON.stringify(e)}`, {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||
import { useUpdaterPlatformSupported } from "@/hooks/use-updater";
|
||||
import { UpdaterManifestAtom } from "@/store/updater";
|
||||
import { UpdaterInstanceAtom } from "@/store/updater";
|
||||
import { formatError } from "@/utils";
|
||||
import { message } from "@/utils/notification";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { BaseCard } from "@nyanpasu/ui";
|
||||
import { version } from "@root/package.json";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||
import { LabelSwitch } from "./modules/clash-field";
|
||||
|
||||
const AutoCheckUpdate = () => {
|
||||
@@ -48,28 +48,28 @@ export const SettingNyanpasuVersion = () => {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom);
|
||||
const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom);
|
||||
const isPlatformSupported = useUpdaterPlatformSupported();
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const info = await checkUpdate();
|
||||
const update = await checkUpdate();
|
||||
|
||||
if (!info?.shouldUpdate) {
|
||||
if (!update?.available) {
|
||||
message(t("No update available."), {
|
||||
title: t("Info"),
|
||||
type: "info",
|
||||
kind: "info",
|
||||
});
|
||||
} else {
|
||||
setUpdaterManifest(info.manifest || null);
|
||||
setUpdaterInstance(update || null);
|
||||
}
|
||||
} catch (e) {
|
||||
message(
|
||||
`Update check failed. Please verify your network connection.\n\n${formatError(e)}`,
|
||||
{
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
|
@@ -44,7 +44,7 @@ export const SettingSystemProxy = () => {
|
||||
} catch (e) {
|
||||
message(`Activation failed!`, {
|
||||
title: t("Error"),
|
||||
type: "error",
|
||||
kind: "error",
|
||||
});
|
||||
} finally {
|
||||
loading[key] = false;
|
||||
|
@@ -74,7 +74,7 @@ export const SettingSystemService = () => {
|
||||
}: ${formatError(e)}`;
|
||||
|
||||
message(errorMessage, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
// If install failed show a prompt to user to install the service manually
|
||||
@@ -109,7 +109,7 @@ export const SettingSystemService = () => {
|
||||
: `Start failed: ${formatError(e)}`;
|
||||
|
||||
message(errorMessage, {
|
||||
type: "error",
|
||||
kind: "error",
|
||||
title: t("Error"),
|
||||
});
|
||||
// If start failed show a prompt to user to start the service manually
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { UpdaterManifestAtom } from "@/store/updater";
|
||||
import { UpdaterInstanceAtom } from "@/store/updater";
|
||||
|
||||
const UpdaterDialog = lazy(() => import("./updater-dialog"));
|
||||
|
||||
export const UpdaterDialogWrapper = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [manifest, setManifest] = useAtom(UpdaterManifestAtom);
|
||||
const [manifest, setManifest] = useAtom(UpdaterInstanceAtom);
|
||||
if (!manifest) return null;
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
@@ -16,7 +16,7 @@ export const UpdaterDialogWrapper = () => {
|
||||
setOpen(false);
|
||||
setManifest(null);
|
||||
}}
|
||||
manifest={manifest}
|
||||
update={manifest}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
@@ -1,44 +1,62 @@
|
||||
import { useLockFn } from "ahooks";
|
||||
import dayjs from "dayjs";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { lazy, Suspense, useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UpdaterIgnoredAtom } from "@/store/updater";
|
||||
import { formatError } from "@/utils";
|
||||
import { message } from "@/utils/notification";
|
||||
import { Button } from "@mui/material";
|
||||
import { Button, LinearProgress } from "@mui/material";
|
||||
import { cleanupProcesses, openThat } from "@nyanpasu/interface";
|
||||
import { BaseDialog, BaseDialogProps, cn } from "@nyanpasu/ui";
|
||||
import { relaunch } from "@tauri-apps/api/process";
|
||||
import { open as openThat } from "@tauri-apps/api/shell";
|
||||
import { installUpdate, type UpdateManifest } from "@tauri-apps/api/updater";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||
import styles from "./updater-dialog.module.scss";
|
||||
|
||||
const Markdown = lazy(() => import("react-markdown"));
|
||||
|
||||
export interface UpdaterDialogProps extends Omit<BaseDialogProps, "title"> {
|
||||
manifest: UpdateManifest;
|
||||
update: Update;
|
||||
}
|
||||
|
||||
export default function UpdaterDialog({
|
||||
open,
|
||||
manifest,
|
||||
update,
|
||||
onClose,
|
||||
...others
|
||||
}: UpdaterDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const setUpdaterIgnore = useSetAtom(UpdaterIgnoredAtom);
|
||||
const [contentLength, setContentLength] = useState(0);
|
||||
const [contentDownloaded, setContentDownloaded] = useState(0);
|
||||
const progress =
|
||||
contentDownloaded && contentLength
|
||||
? (contentDownloaded / contentLength) * 100
|
||||
: 0;
|
||||
const onDownloadEvent = useCallback((e: DownloadEvent) => {
|
||||
switch (e.event) {
|
||||
case "Started":
|
||||
setContentLength(e.data.contentLength || 0);
|
||||
break;
|
||||
case "Progress":
|
||||
setContentDownloaded((prev) => prev + e.data.chunkLength);
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleUpdate = useLockFn(async () => {
|
||||
try {
|
||||
// Install the update. This will also restart the app on Windows!
|
||||
await installUpdate();
|
||||
|
||||
await update.download(onDownloadEvent);
|
||||
await cleanupProcesses();
|
||||
// cleanup and stop core
|
||||
await update.install();
|
||||
// On macOS and Linux you will need to restart the app manually.
|
||||
// You could use this step to display another confirmation dialog.
|
||||
await relaunch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message(formatError(e), { type: "error", title: t("Error") });
|
||||
message(formatError(e), { kind: "error", title: t("Error") });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,7 +66,7 @@ export default function UpdaterDialog({
|
||||
title={t("updater.title")}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setUpdaterIgnore(manifest.version); // TODO: control this behavior
|
||||
setUpdaterIgnore(update.version); // TODO: control this behavior
|
||||
onClose?.();
|
||||
}}
|
||||
onOk={handleUpdate}
|
||||
@@ -64,9 +82,9 @@ export default function UpdaterDialog({
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-2">
|
||||
<div className="flex gap-3">
|
||||
<span className="text-xl font-bold">{manifest.version}</span>
|
||||
<span className="text-xl font-bold">{update.version}</span>
|
||||
<span className="text-xs text-slate-500">
|
||||
{dayjs(manifest.date, "YYYY-MM-DD H:mm:ss Z").format(
|
||||
{dayjs(update.date, "YYYY-MM-DD H:mm:ss Z").format(
|
||||
"YYYY-MM-DD HH:mm:ss",
|
||||
)}
|
||||
</span>
|
||||
@@ -76,7 +94,7 @@ export default function UpdaterDialog({
|
||||
size="small"
|
||||
onClick={() => {
|
||||
openThat(
|
||||
`https://github.com/LibNyanpasu/clash-nyanpasu/releases/tag/v${manifest.version}`,
|
||||
`https://github.com/LibNyanpasu/clash-nyanpasu/releases/tag/v${update.version}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -106,10 +124,22 @@ export default function UpdaterDialog({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{manifest.body || "New version available."}
|
||||
{update.body || "New version available."}
|
||||
</Markdown>
|
||||
</Suspense>
|
||||
</div>
|
||||
{contentLength && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<LinearProgress
|
||||
className="flex-1"
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
/>
|
||||
<span className="text-xs text-slate-500">
|
||||
{progress.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { OS } from "@/consts";
|
||||
import { UpdaterIgnoredAtom, UpdaterManifestAtom } from "@/store/updater";
|
||||
import { UpdaterIgnoredAtom, UpdaterInstanceAtom } from "@/store/updater";
|
||||
import { useNyanpasu } from "@nyanpasu/interface";
|
||||
import { checkUpdate } from "@tauri-apps/api/updater";
|
||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||
import { useIsAppImage } from "./use-consts";
|
||||
|
||||
export function useUpdaterPlatformSupported() {
|
||||
@@ -26,15 +26,15 @@ export function useUpdaterPlatformSupported() {
|
||||
export default function useUpdater() {
|
||||
const { nyanpasuConfig } = useNyanpasu();
|
||||
const updaterIgnored = useAtomValue(UpdaterIgnoredAtom);
|
||||
const setUpdaterManifest = useSetAtom(UpdaterManifestAtom);
|
||||
const setUpdaterInstance = useSetAtom(UpdaterInstanceAtom);
|
||||
const isPlatformSupported = useUpdaterPlatformSupported();
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
if (nyanpasuConfig?.enable_auto_check_update && isPlatformSupported) {
|
||||
const info = await checkUpdate();
|
||||
if (info?.shouldUpdate && updaterIgnored !== info.manifest?.version) {
|
||||
setUpdaterManifest(info.manifest || null);
|
||||
const updater = await checkUpdate();
|
||||
if (updater?.available && updaterIgnored !== updater?.version) {
|
||||
setUpdaterInstance(updater || null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export default function useUpdater() {
|
||||
}, [
|
||||
isPlatformSupported,
|
||||
nyanpasuConfig?.enable_auto_check_update,
|
||||
setUpdaterManifest,
|
||||
setUpdaterInstance,
|
||||
updaterIgnored,
|
||||
]);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user