diff --git a/.github/update.log b/.github/update.log index 5b7161ab50..66b87da0ba 100644 --- a/.github/update.log +++ b/.github/update.log @@ -770,3 +770,4 @@ Update On Tue Sep 17 20:34:06 CEST 2024 Update On Wed Sep 18 20:34:25 CEST 2024 Update On Thu Sep 19 20:35:43 CEST 2024 Update On Fri Sep 20 20:34:37 CEST 2024 +Update On Sat Sep 21 20:33:09 CEST 2024 diff --git a/brook/README.md b/brook/README.md index 1990da0f97..c24d9f9bfb 100644 --- a/brook/README.md +++ b/brook/README.md @@ -23,12 +23,20 @@ brook server -l :9999 -p hello ## Client -| iOS | Android | Mac |Windows |Linux |OpenWrt | -| --- | --- | --- | --- | --- | --- | -| [![](https://brook.app/images/appstore.png)](https://apps.apple.com/us/app/brook-network-tool/id1216002642) | [![](https://brook.app/images/android.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.apk) | [![](https://brook.app/images/mac.png)](https://apps.apple.com/us/app/brook-network-tool/id1216002642) | [![Windows](https://brook.app/images/windows.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.msix) | [![](https://brook.app/images/linux.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.bin) | [![OpenWrt](https://brook.app/images/openwrt.png)](https://github.com/txthinking/brook/releases) | -| / | / | [App Mode](https://www.txthinking.com/talks/articles/macos-app-mode-en.article) | [How](https://www.txthinking.com/talks/articles/msix-brook-en.article) | [How](https://www.txthinking.com/talks/articles/linux-app-brook-en.article) | [How](https://www.txthinking.com/talks/articles/brook-openwrt-en.article) | +- [iOS](https://apps.apple.com/us/app/brook-network-tool/id1216002642) +- [Android](https://github.com/txthinking/brook/releases/latest/download/Brook.apk) +- [macOS](https://apps.apple.com/us/app/brook-network-tool/id1216002642) +- [Windows](https://github.com/txthinking/brook/releases/latest/download/Brook.msix) +- [Linux](https://github.com/txthinking/brook/releases/latest/download/Brook.bin) +- [OpenWrt](https://github.com/txthinking/brook/releases) > You may want to use `brook link` to customize some parameters + +- [About App Mode on macOS](https://www.txthinking.com/talks/articles/macos-app-mode-en.article) +- [How to install Brook on Windows?](https://www.txthinking.com/talks/articles/msix-brook-en.article) +- [How to install Brook on Linux](https://www.txthinking.com/talks/articles/linux-app-brook-en.article) +- [How to install Brook on OpenWrt](https://www.txthinking.com/talks/articles/brook-openwrt-en.article) + # Client Brook GUI will pass different _global variables_ to the script at different times, and the script only needs to assign the processing result to the global variable `out` @@ -449,7 +457,7 @@ Brook [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] - **--dialWithSocks5Username**="": If there is -- **--help, -h**: show help + - **--ipLimitInterval**="": Interval (s) for ipLimitMax (default: 0) @@ -1028,11 +1036,11 @@ Generate markdown page - **--file, -f**="": Write to file, default print to stdout -- **--help, -h**: show help -### help, h -Shows a list of commands or help for one command + + + ## manpage @@ -1045,6 +1053,7 @@ Generate man.1 page ## help, h Shows a list of commands or help for one command + # Examples List some examples of common scene commands, pay attention to replace the parameters such as IP, port, password, domain name, certificate path, etc. in the example by yourself diff --git a/brook/docs/build.js b/brook/docs/build.js new file mode 100755 index 0000000000..50c17fe97d --- /dev/null +++ b/brook/docs/build.js @@ -0,0 +1,30 @@ +#!/usr/bin/env bun + +import * as fs from 'node:fs/promises' +import { $ } from 'bun' + +var f = await fs.open("../readme.md", 'w+'); +await fs.write(f.fd, '# Brook\n') +await fs.write(f.fd, '\n') +await fs.write(f.fd, '\n') +await fs.write(f.fd, 'A cross-platform programmable network tool.\n') +await fs.write(f.fd, '\n') +await fs.write(f.fd, '# Sponsor\n') +await fs.write(f.fd, '**❤️ [Shiliew - A network app designed for those who value their time](https://www.txthinking.com/shiliew.html)**\n') + +var s = await fs.readFile('getting-started.md', { encoding: 'utf8' }) +await fs.write(f.fd, s) +var s = await fs.readFile('gui.md', { encoding: 'utf8' }) +await fs.write(f.fd, s) +await fs.write(f.fd, '# CLI Documentation\n') +await fs.write(f.fd, 'Each subcommand has a `--example` parameter that can print the minimal example of usage\n') +var s = await $`brook mdpage`.text() +s = s.split("\n").filter(v => !v.startsWith("[")).join("\n").replace("```\n```", "```\nbrook --help\n```").split("\n").map(v => v.startsWith("**") && !v.startsWith("**Usage") ? "- " + v : v).join('\n') +s = s.replace("### help, h", "").replace("Shows a list of commands or help for one command", "").replaceAll("- **--help, -h**: show help", "") +await fs.write(f.fd, s) +var s = await fs.readFile('example.md', { encoding: 'utf8' }) +await fs.write(f.fd, s) +var s = await fs.readFile('resources.md', { encoding: 'utf8' }) +await fs.write(f.fd, s) +await fs.close(f.fd) +await $`markdown ../readme.md ./index.html` diff --git a/brook/docs/build.sh b/brook/docs/build.sh deleted file mode 100755 index 4e8075312a..0000000000 --- a/brook/docs/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -echo '# Brook' > ../readme.md -echo '' >> ../readme.md -echo '' >> ../readme.md -echo 'A cross-platform programmable network tool.' >> ../readme.md -echo '' >> ../readme.md -echo '# Sponsor' >> ../readme.md -echo '**❤️ [Shiliew - A network app designed for those who value their time](https://www.txthinking.com/shiliew.html)**' >> ../readme.md - -cat getting-started.md >> ../readme.md -cat gui.md >> ../readme.md - -echo '# CLI Documentation' >> ../readme.md -echo 'Each subcommand has a `--example` parameter that can print the minimal example of usage' >> ../readme.md -jb '$1`brook mdpage`.split("\n").filter(v=>!v.startsWith("[")).join("\n").replace("```\n```", "```\nbrook --help\n```").split("\n").forEach(v=> echo(v.startsWith("**") && !v.startsWith("**Usage") ? "- "+v : v))' >> ../readme.md - -cat example.md >> ../readme.md -cat resources.md >> ../readme.md - -markdown ../readme.md ./index.html - diff --git a/brook/docs/getting-started.md b/brook/docs/getting-started.md index 2ccb15bb82..7b929673c8 100644 --- a/brook/docs/getting-started.md +++ b/brook/docs/getting-started.md @@ -16,9 +16,17 @@ brook server -l :9999 -p hello ## Client -| iOS | Android | Mac |Windows |Linux |OpenWrt | -| --- | --- | --- | --- | --- | --- | -| [![](https://brook.app/images/appstore.png)](https://apps.apple.com/us/app/brook-network-tool/id1216002642) | [![](https://brook.app/images/android.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.apk) | [![](https://brook.app/images/mac.png)](https://apps.apple.com/us/app/brook-network-tool/id1216002642) | [![Windows](https://brook.app/images/windows.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.msix) | [![](https://brook.app/images/linux.png)](https://github.com/txthinking/brook/releases/latest/download/Brook.bin) | [![OpenWrt](https://brook.app/images/openwrt.png)](https://github.com/txthinking/brook/releases) | -| / | / | [App Mode](https://www.txthinking.com/talks/articles/macos-app-mode-en.article) | [How](https://www.txthinking.com/talks/articles/msix-brook-en.article) | [How](https://www.txthinking.com/talks/articles/linux-app-brook-en.article) | [How](https://www.txthinking.com/talks/articles/brook-openwrt-en.article) | +- [iOS](https://apps.apple.com/us/app/brook-network-tool/id1216002642) +- [Android](https://github.com/txthinking/brook/releases/latest/download/Brook.apk) +- [macOS](https://apps.apple.com/us/app/brook-network-tool/id1216002642) +- [Windows](https://github.com/txthinking/brook/releases/latest/download/Brook.msix) +- [Linux](https://github.com/txthinking/brook/releases/latest/download/Brook.bin) +- [OpenWrt](https://github.com/txthinking/brook/releases) > You may want to use `brook link` to customize some parameters + +- [About App Mode on macOS](https://www.txthinking.com/talks/articles/macos-app-mode-en.article) +- [How to install Brook on Windows?](https://www.txthinking.com/talks/articles/msix-brook-en.article) +- [How to install Brook on Linux](https://www.txthinking.com/talks/articles/linux-app-brook-en.article) +- [How to install Brook on OpenWrt](https://www.txthinking.com/talks/articles/brook-openwrt-en.article) + diff --git a/brook/docs/index.html b/brook/docs/index.html index dcb70cb0a0..6600dbce5d 100644 --- a/brook/docs/index.html +++ b/brook/docs/index.html @@ -1216,12 +1216,9 @@
  • echoclient
  • ipcountry
  • completion
  • -
  • mdpage -
  • +
  • mdpage
  • manpage
  • -
  • help, h
  • +
  • help, h
  • Examples
  • Examples -

    help, h

    -

    Shows a list of commands or help for one command

    manpage

    Generate man.1 page

    -

    help, h

    +

    help, h

    Shows a list of commands or help for one command

    Examples

    List some examples of common scene commands, pay attention to replace the parameters such as IP, port, password, domain name, certificate path, etc. in the example by yourself

    diff --git a/clash-meta/adapter/provider/provider.go b/clash-meta/adapter/provider/provider.go index 3103b50828..4381c24d9d 100644 --- a/clash-meta/adapter/provider/provider.go +++ b/clash-meta/adapter/provider/provider.go @@ -128,7 +128,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() { go func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() - resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().Url(), http.MethodGet, nil, nil, pp.Vehicle().Proxy()) if err != nil { return @@ -137,7 +137,7 @@ func (pp *proxySetProvider) getSubscriptionInfo() { userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo")) if userInfoStr == "" { - resp2, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(), + resp2, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().Url(), http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil, pp.Vehicle().Proxy()) if err != nil { return diff --git a/clash-meta/component/resource/fetcher.go b/clash-meta/component/resource/fetcher.go index 9a4f5a7591..fec9fe7714 100644 --- a/clash-meta/component/resource/fetcher.go +++ b/clash-meta/component/resource/fetcher.go @@ -65,8 +65,7 @@ func (f *Fetcher[V]) Initial() (V, error) { modTime := stat.ModTime() f.updatedAt = modTime isLocal = true - if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) { - log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) + if time.Since(modTime) > f.interval { forceUpdate = true } } else { @@ -78,21 +77,7 @@ func (f *Fetcher[V]) Initial() (V, error) { return lo.Empty[V](), err } - var contents V - if forceUpdate { - var forceBuf []byte - if forceBuf, err = f.vehicle.Read(f.ctx); err == nil { - if contents, err = f.parser(forceBuf); err == nil { - isLocal = false - buf = forceBuf - } - } - } - - if err != nil || !forceUpdate { - contents, err = f.parser(buf) - } - + contents, err := f.parser(buf) if err != nil { if !isLocal { return lo.Empty[V](), err @@ -135,7 +120,7 @@ func (f *Fetcher[V]) Initial() (V, error) { return lo.Empty[V](), err } } else if f.interval > 0 { - go f.pullLoop() + go f.pullLoop(forceUpdate) } return contents, nil @@ -164,7 +149,7 @@ func (f *Fetcher[V]) SideUpdate(buf []byte) (V, bool, error) { } if f.vehicle.Type() != types.File { - if err := safeWrite(f.vehicle.Path(), buf); err != nil { + if err = safeWrite(f.vehicle.Path(), buf); err != nil { return lo.Empty[V](), false, err } } @@ -183,12 +168,17 @@ func (f *Fetcher[V]) Close() error { return nil } -func (f *Fetcher[V]) pullLoop() { +func (f *Fetcher[V]) pullLoop(forceUpdate bool) { initialInterval := f.interval - time.Since(f.updatedAt) if initialInterval > f.interval { initialInterval = f.interval } + if forceUpdate { + log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name()) + f.update(f.vehicle.Path()) + } + timer := time.NewTimer(initialInterval) defer timer.Stop() for { diff --git a/clash-meta/component/resource/vehicle.go b/clash-meta/component/resource/vehicle.go index 4618ef52f2..74324e6d3b 100644 --- a/clash-meta/component/resource/vehicle.go +++ b/clash-meta/component/resource/vehicle.go @@ -24,6 +24,10 @@ func (f *FileVehicle) Path() string { return f.path } +func (f *FileVehicle) Url() string { + return "file://" + f.path +} + func (f *FileVehicle) Read(ctx context.Context) ([]byte, error) { return os.ReadFile(f.path) } diff --git a/clash-meta/component/trie/domain.go b/clash-meta/component/trie/domain.go index 87dfeda60d..574a59caa7 100644 --- a/clash-meta/component/trie/domain.go +++ b/clash-meta/component/trie/domain.go @@ -3,6 +3,8 @@ package trie import ( "errors" "strings" + "unicode" + "unicode/utf8" ) const ( @@ -25,6 +27,14 @@ func ValidAndSplitDomain(domain string) ([]string, bool) { if domain != "" && domain[len(domain)-1] == '.' { return nil, false } + if domain != "" { + if r, _ := utf8.DecodeRuneInString(domain); unicode.IsSpace(r) { + return nil, false + } + if r, _ := utf8.DecodeLastRuneInString(domain); unicode.IsSpace(r) { + return nil, false + } + } domain = strings.ToLower(domain) parts := strings.Split(domain, domainStep) if len(parts) == 1 { diff --git a/clash-meta/component/trie/domain_test.go b/clash-meta/component/trie/domain_test.go index 916f61076d..6aab72d3a7 100644 --- a/clash-meta/component/trie/domain_test.go +++ b/clash-meta/component/trie/domain_test.go @@ -127,3 +127,14 @@ func TestTrie_Foreach(t *testing.T) { }) assert.Equal(t, 7, count) } + +func TestTrie_Space(t *testing.T) { + validDomain := func(domain string) bool { + _, ok := trie.ValidAndSplitDomain(domain) + return ok + } + assert.True(t, validDomain("google.com")) + assert.False(t, validDomain(" google.com")) + assert.False(t, validDomain(" google.com ")) + assert.True(t, validDomain("Mijia Cloud")) +} diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go index 907d40bd37..5a95de79d6 100644 --- a/clash-meta/config/config.go +++ b/clash-meta/config/config.go @@ -8,7 +8,6 @@ import ( "net/netip" "net/url" "path" - "regexp" "strings" "time" @@ -1287,7 +1286,6 @@ func parsePureDNSServer(server string) string { func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], ruleProviders map[string]providerTypes.RuleProvider, respectRules bool, preferH3 bool) ([]dns.Policy, error) { var policy []dns.Policy - re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) for pair := nsPolicy.Oldest(); pair != nil; pair = pair.Next() { k, v := pair.Key, pair.Value @@ -1299,8 +1297,9 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro if err != nil { return nil, err } - if strings.Contains(strings.ToLower(k), ",") { - if strings.Contains(k, "geosite:") { + kLower := strings.ToLower(k) + if strings.Contains(kLower, ",") { + if strings.Contains(kLower, "geosite:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1308,7 +1307,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro newKey := "geosite:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if strings.Contains(strings.ToLower(k), "rule-set:") { + } else if strings.Contains(kLower, "rule-set:") { subkeys := strings.Split(k, ":") subkeys = subkeys[1:] subkeys = strings.Split(subkeys[0], ",") @@ -1316,16 +1315,16 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro newKey := "rule-set:" + subkey policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) } - } else if re.MatchString(k) { + } else { subkeys := strings.Split(k, ",") for _, subkey := range subkeys { policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) } } } else { - if strings.Contains(strings.ToLower(k), "geosite:") { + if strings.Contains(kLower, "geosite:") { policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) - } else if strings.Contains(strings.ToLower(k), "rule-set:") { + } else if strings.Contains(kLower, "rule-set:") { policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) } else { policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) diff --git a/clash-meta/constant/provider/interface.go b/clash-meta/constant/provider/interface.go index 880bdadff6..9d24f6917d 100644 --- a/clash-meta/constant/provider/interface.go +++ b/clash-meta/constant/provider/interface.go @@ -34,6 +34,7 @@ func (v VehicleType) String() string { type Vehicle interface { Read(ctx context.Context) ([]byte, error) Path() string + Url() string Proxy() string Type() VehicleType } diff --git a/clash-meta/listener/sing_tun/server.go b/clash-meta/listener/sing_tun/server.go index a135a30192..c2c668b34e 100644 --- a/clash-meta/listener/sing_tun/server.go +++ b/clash-meta/listener/sing_tun/server.go @@ -138,6 +138,7 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis tunName := options.Device if options.FileDescriptor == 0 && (tunName == "" || !checkTunName(tunName)) { tunName = CalculateInterfaceName(InterfaceName) + options.Device = tunName } routeAddress := options.RouteAddress if len(options.Inet4RouteAddress) > 0 { diff --git a/clash-nyanpasu/backend/Cargo.lock b/clash-nyanpasu/backend/Cargo.lock index 6923d5a0f6..9b62b8698d 100644 --- a/clash-nyanpasu/backend/Cargo.lock +++ b/clash-nyanpasu/backend/Cargo.lock @@ -570,9 +570,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", "axum-core", @@ -599,7 +599,7 @@ dependencies = [ "sync_wrapper 1.0.1", "tokio", "tokio-tungstenite", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -607,9 +607,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" dependencies = [ "async-trait", "bytes", @@ -620,7 +620,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -1228,9 +1228,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -1238,9 +1238,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -1250,9 +1250,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1756,9 +1756,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.75+curl-8.10.0" +version = "0.4.76+curl-8.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4fd752d337342e4314717c0d9b6586b059a120c80029ebe4d49b11fec7875e" +checksum = "00462dbe9cbb9344e1b2be34d9094d74e3b8aac59a883495b335eafd02e25120" dependencies = [ "cc", "libc", @@ -3322,7 +3322,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.7", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -4709,7 +4709,7 @@ dependencies = [ [[package]] name = "nyanpasu-ipc" version = "1.0.6" -source = "git+https://github.com/LibNyanpasu/nyanpasu-service.git#a9d91584893489ffb96741bfee3f5b20a4f2eba7" +source = "git+https://github.com/LibNyanpasu/nyanpasu-service.git#eafde94f159754296b7bf896b08f8c50da6bfe05" dependencies = [ "anyhow", "axum", @@ -5583,9 +5583,9 @@ checksum = "325a6d2ac5dee293c3b2612d4993b98aec1dff096b0a2dae70ed7d95784a05da" [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" [[package]] name = "powerfmt" @@ -5750,9 +5750,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] @@ -6446,9 +6446,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -7439,7 +7439,7 @@ dependencies = [ [[package]] name = "tauri-plugin-clipboard-manager" version = "2.0.0-rc.4" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "arboard", "image", @@ -7469,7 +7469,7 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" version = "2.0.0-rc.7" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "log", "raw-window-handle", @@ -7486,7 +7486,7 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" version = "2.0.0-rc.5" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "anyhow", "dunce", @@ -7506,7 +7506,7 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" version = "2.0.0-rc.2" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "global-hotkey", "log", @@ -7520,7 +7520,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notification" version = "2.0.0-rc.5" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "log", "notify-rust", @@ -7538,7 +7538,7 @@ dependencies = [ [[package]] name = "tauri-plugin-os" version = "2.0.0-rc.1" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "gethostname 0.5.0", "log", @@ -7555,7 +7555,7 @@ dependencies = [ [[package]] name = "tauri-plugin-process" version = "2.0.0-rc.1" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "tauri", "tauri-plugin", @@ -7564,7 +7564,7 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" version = "2.0.0-rc.3" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "encoding_rs", "log", @@ -7584,7 +7584,7 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" version = "2.0.0-rc.3" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#6bf1bd8d44bb95618590aa066e638509b014e0f9" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#221f50f53bd7a87dbd404e4cb1aaf502a5047785" dependencies = [ "base64 0.22.1", "dirs 5.0.1", @@ -7593,6 +7593,7 @@ dependencies = [ "http 1.1.0", "infer 0.16.0", "minisign-verify", + "percent-encoding", "reqwest", "semver 1.0.23", "serde", @@ -8031,9 +8032,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", @@ -8126,6 +8127,21 @@ dependencies = [ "tokio", "tower-layer", "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", "tracing", ] @@ -8290,9 +8306,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", @@ -8303,7 +8319,6 @@ dependencies = [ "rand 0.8.5", "sha1", "thiserror", - "url", "utf-8", ] @@ -8803,7 +8818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.36.1", + "quick-xml 0.36.2", "quote", ] diff --git a/clash-nyanpasu/backend/tauri/src/core/hotkey.rs b/clash-nyanpasu/backend/tauri/src/core/hotkey.rs index 89600623cd..a6c14486ba 100644 --- a/clash-nyanpasu/backend/tauri/src/core/hotkey.rs +++ b/clash-nyanpasu/backend/tauri/src/core/hotkey.rs @@ -5,7 +5,7 @@ use parking_lot::Mutex; use std::{collections::HashMap, sync::Arc}; use tauri::AppHandle; -use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut}; +use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState}; pub struct Hotkey { current: Arc>>, // 保存当前的热键设置 @@ -15,6 +15,7 @@ pub struct Hotkey { // (hotkey, func) type HotKeyOp<'a> = (&'a str, HotKeyOpType<'a>); +#[derive(Debug)] enum HotKeyOpType<'a> { #[allow(unused)] Unbind(&'a str), @@ -97,8 +98,10 @@ impl Hotkey { _ => bail!("invalid function \"{func}\""), }; - manager.on_shortcut(hotkey, move |_app_handle, _hotkey, _ev| { - f(); + manager.on_shortcut(hotkey, move |_app_handle, _hotkey, ev| { + if let ShortcutState::Pressed = ev.state { + f(); + } })?; log::info!(target: "app", "register hotkey {hotkey} {func}"); @@ -117,6 +120,7 @@ impl Hotkey { Ok(()) } + #[tracing::instrument(skip(self))] pub fn update(&self, new_hotkeys: Vec) -> Result<()> { let mut current = self.current.lock(); let old_map = Self::get_map_from_vec(¤t); @@ -126,11 +130,13 @@ impl Hotkey { // 先检查一遍所有新的热键是不是可以用的 for (hotkey, op) in ops.iter() { - if let HotKeyOpType::Bind(_) = op { + if matches!(op, HotKeyOpType::Bind(_) | HotKeyOpType::Change(_, _)) { Self::check_key(hotkey)? } } + tracing::info!("hotkey update: {:?}", ops); + for (hotkey, op) in ops.iter() { match op { HotKeyOpType::Unbind(_) => self.unregister(hotkey)?, diff --git a/clash-nyanpasu/backend/tauri/src/lib.rs b/clash-nyanpasu/backend/tauri/src/lib.rs index 469d6204f7..85c4fa9011 100644 --- a/clash-nyanpasu/backend/tauri/src/lib.rs +++ b/clash-nyanpasu/backend/tauri/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(auto_traits, negative_impls)] +#![feature(auto_traits, negative_impls, let_chains)] #![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" @@ -136,6 +136,14 @@ pub fn run() -> std::io::Result<()> { panic.note = note, "A panic occurred", ); + + // This is a workaround for the upstream issue: https://github.com/tauri-apps/tauri/issues/10546 + if let Some(s) = payload.as_ref() + && s.contains("PostMessage failed ; is the messages queue full?") + { + return; + } + utils::dialog::panic_dialog(&format!( "payload: {:#?}\nlocation: {:?}\nbacktrace: {:#?}\n\nnote: {:?}", payload, location, backtrace, note @@ -149,6 +157,7 @@ pub fn run() -> std::io::Result<()> { }); let _ = task.join(); default_panic(panic_info); + std::process::exit(1); // exit if default panic handler doesn't exit })); let verge = { Config::verge().latest().language.clone().unwrap() }; @@ -324,10 +333,13 @@ pub fn run() -> std::io::Result<()> { tauri::WindowEvent::ScaleFactorChanged { scale_factor, .. } => { core::tray::on_scale_factor_changed(scale_factor); } - tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed => { + tauri::WindowEvent::CloseRequested { .. } => { log::debug!(target: "app", "window close requested"); - reset_window_open_counter(); let _ = resolve::save_window_state(app_handle, true); + } + tauri::WindowEvent::Destroyed => { + log::debug!(target: "app", "window destroyed"); + reset_window_open_counter(); #[cfg(target_os = "macos")] log_err!(app_handle.run_on_main_thread(|| { crate::utils::dock::macos::hide_dock_icon(); diff --git a/clash-nyanpasu/backend/tauri/src/utils/init/mod.rs b/clash-nyanpasu/backend/tauri/src/utils/init/mod.rs index b61fd8f8d5..b416ce732e 100644 --- a/clash-nyanpasu/backend/tauri/src/utils/init/mod.rs +++ b/clash-nyanpasu/backend/tauri/src/utils/init/mod.rs @@ -104,27 +104,27 @@ pub fn run_pending_migrations() -> Result<()> { /// before tauri setup pub fn init_config() -> Result<()> { // Check if old config dir exist and new config dir is not exist - let mut old_app_dir: Option = None; - let mut app_dir: Option = None; - crate::dialog_err!(dirs::old_app_home_dir().map(|_old_app_dir| { - old_app_dir = Some(_old_app_dir); - })); + // let mut old_app_dir: Option = None; + // let mut app_dir: Option = None; + // crate::dialog_err!(dirs::old_app_home_dir().map(|_old_app_dir| { + // old_app_dir = Some(_old_app_dir); + // })); - crate::dialog_err!(dirs::app_home_dir().map(|_app_dir| { - app_dir = Some(_app_dir); - })); + // crate::dialog_err!(dirs::app_home_dir().map(|_app_dir| { + // app_dir = Some(_app_dir); + // })); - if let (Some(app_dir), Some(old_app_dir)) = (app_dir, old_app_dir) { - let msg = t!("dialog.migrate"); - if !app_dir.exists() && old_app_dir.exists() && migrate_dialog(msg.to_string().as_str()) { - if let Err(e) = do_config_migration(&old_app_dir, &app_dir) { - super::dialog::error_dialog(format!("failed to do migration: {:?}", e)) - } - } - if !app_dir.exists() { - let _ = fs::create_dir_all(app_dir); - } - } + // if let (Some(app_dir), Some(old_app_dir)) = (app_dir, old_app_dir) { + // let msg = t!("dialog.migrate"); + // if !app_dir.exists() && old_app_dir.exists() && migrate_dialog(msg.to_string().as_str()) { + // if let Err(e) = do_config_migration(&old_app_dir, &app_dir) { + // super::dialog::error_dialog(format!("failed to do migration: {:?}", e)) + // } + // } + // if !app_dir.exists() { + // let _ = fs::create_dir_all(app_dir); + // } + // } // init log logging::init().unwrap(); diff --git a/clash-nyanpasu/backend/tauri/tauri.conf.json b/clash-nyanpasu/backend/tauri/tauri.conf.json index 141216bcf9..f890a3fc1b 100644 --- a/clash-nyanpasu/backend/tauri/tauri.conf.json +++ b/clash-nyanpasu/backend/tauri/tauri.conf.json @@ -37,6 +37,7 @@ "sidecar/mihomo", "sidecar/mihomo-alpha", "sidecar/clash-rs", + "sidecar/clash-rs-alpha", "sidecar/nyanpasu-service" ], "copyright": "© 2024 Clash Nyanpasu All Rights Reserved", diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index 49f3bd0ef8..f0a8a8fe0a 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -13,7 +13,7 @@ "dependencies": { "@tauri-apps/api": "2.0.0-rc.5", "ahooks": "3.8.1", - "ofetch": "1.3.4", + "ofetch": "1.4.0", "react": "rc", "swr": "2.2.5" }, diff --git a/clash-nyanpasu/frontend/interface/src/ipc/useNyanpasu.ts b/clash-nyanpasu/frontend/interface/src/ipc/useNyanpasu.ts index 71dce178b0..6606781395 100644 --- a/clash-nyanpasu/frontend/interface/src/ipc/useNyanpasu.ts +++ b/clash-nyanpasu/frontend/interface/src/ipc/useNyanpasu.ts @@ -38,6 +38,8 @@ export const useNyanpasu = (options?: { } catch (error) { if (options?.onError) { options?.onError(error); + } else { + throw error; } } }; diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 27398b203e..f0fb724c7a 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -34,7 +34,7 @@ "json-schema": "0.4.0", "material-react-table": "3.0.1", "monaco-editor": "0.52.0", - "mui-color-input": "4.0.0", + "mui-color-input": "4.0.1", "react": "rc", "react-dom": "rc", "react-error-boundary": "4.0.13", @@ -53,7 +53,7 @@ "@csstools/normalize.css": "12.1.1", "@emotion/babel-plugin": "11.12.0", "@emotion/react": "11.13.3", - "@iconify/json": "2.2.250", + "@iconify/json": "2.2.251", "@monaco-editor/react": "4.6.0", "@tanstack/react-router": "1.58.3", "@tanstack/router-devtools": "1.58.3", @@ -75,13 +75,13 @@ "meta-json-schema": "1.18.8", "monaco-yaml": "5.2.2", "nanoid": "5.0.7", - "sass": "1.79.2", + "sass": "1.79.3", "shiki": "1.18.0", "tailwindcss-textshadow": "2.1.3", "unplugin-auto-import": "0.18.3", "unplugin-icons": "0.19.3", "validator": "13.12.0", - "vite": "5.4.6", + "vite": "5.4.7", "vite-plugin-sass-dts": "1.3.29", "vite-plugin-svgr": "4.2.0", "vite-tsconfig-paths": "5.0.1", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/new-profile-button.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/new-profile-button.tsx index ce0a102f8d..39366e2085 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/new-profile-button.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/new-profile-button.tsx @@ -1,9 +1,9 @@ import { use, useEffect, useState } from "react"; import { Add } from "@mui/icons-material"; -import { FloatingButton } from "@nyanpasu/ui"; +import { cn, FloatingButton } from "@nyanpasu/ui"; import { AddProfileContext, ProfileDialog } from "./profile-dialog"; -export const NewProfileButton = () => { +export const NewProfileButton = ({ className }: { className?: string }) => { const addProfileCtx = use(AddProfileContext); const [open, setOpen] = useState(!!addProfileCtx); useEffect(() => { @@ -11,7 +11,7 @@ export const NewProfileButton = () => { }, [addProfileCtx]); return ( <> - setOpen(true)}> + setOpen(true)}> diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx index 3e5abbc272..b661367424 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx @@ -1,7 +1,7 @@ import { useLockFn, useMemoizedFn, useSetState } from "ahooks"; import dayjs from "dayjs"; import { AnimatePresence, motion } from "framer-motion"; -import { memo, useEffect, useMemo, useState } from "react"; +import { memo, use, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { message } from "@/utils/notification"; import parseTraffic from "@/utils/parse-traffic"; @@ -29,6 +29,7 @@ import { import { Profile, useClash } from "@nyanpasu/interface"; import { cleanDeepClickEvent, cn } from "@nyanpasu/ui"; import { ProfileDialog } from "./profile-dialog"; +import { GlobalUpdatePendingContext } from "./provider"; export interface ProfileItemProps { item: Profile.Item; @@ -60,6 +61,8 @@ export const ProfileItem = memo(function ProfileItem({ viewProfile, } = useClash(); + const globalUpdatePending = use(GlobalUpdatePendingContext); + const [loading, setLoading] = useSetState({ update: false, card: false, @@ -91,16 +94,6 @@ export const ProfileItem = memo(function ProfileItem({ const [anchorEl, setAnchorEl] = useState(null); - const menuMapping = { - Select: () => handleSelect(), - "Edit Info": () => setOpen(true), - "Proxy Chains": () => onClickChains(item), - "Open File": () => viewProfile(item.uid), - Update: () => handleUpdate(), - "Update(Proxy)": () => handleUpdate(true), - Delete: () => handleDelete(), - }; - const handleSelect = useLockFn(async () => { if (selected) { return; @@ -166,7 +159,27 @@ export const ProfileItem = memo(function ProfileItem({ } }); - const MenuComp = () => { + const menuMapping = useMemo( + () => ({ + Select: () => handleSelect(), + "Edit Info": () => setOpen(true), + "Proxy Chains": () => onClickChains(item), + "Open File": () => viewProfile(item.uid), + Update: () => handleUpdate(), + "Update(Proxy)": () => handleUpdate(true), + Delete: () => handleDelete(), + }), + [ + handleDelete, + handleSelect, + handleUpdate, + item, + onClickChains, + viewProfile, + ], + ); + + const MenuComp = useMemo(() => { const handleClick = (func: () => void) => { setAnchorEl(null); func(); @@ -193,7 +206,7 @@ export const ProfileItem = memo(function ProfileItem({ })} ); - }; + }, [anchorEl, menuMapping, t]); const [open, setOpen] = useState(false); @@ -310,7 +323,7 @@ export const ProfileItem = memo(function ProfileItem({ cleanDeepClickEvent(e); menuMapping.Update(); }} - loading={loading.update} + loading={globalUpdatePending || loading.update} > @@ -351,7 +364,7 @@ export const ProfileItem = memo(function ProfileItem({
    Applying Profile...
    - + {MenuComp} setOpen(false)} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/provider.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/provider.tsx new file mode 100644 index 0000000000..bbb51e4620 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/provider.tsx @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const GlobalUpdatePendingContext = createContext(false); diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-dialog.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-dialog.tsx index faf9a1b195..26834cb328 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-dialog.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-dialog.tsx @@ -1,7 +1,8 @@ -import { useLockFn, useMemoizedFn } from "ahooks"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useLockFn } from "ahooks"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { notification, NotificationType } from "@/utils/notification"; +import { formatError } from "@/utils"; +import { message } from "@/utils/notification"; import { Typography } from "@mui/material"; import { useNyanpasu } from "@nyanpasu/interface"; import { BaseDialog, BaseDialogProps } from "@nyanpasu/ui"; @@ -21,7 +22,21 @@ const HOTKEY_FUNC = [ "toggle_tun_mode", // "enable_tun_mode", // "disable_tun_mode", -]; +] as const; + +type AllowedHotkeyFunc = (typeof HOTKEY_FUNC)[number]; + +type Key = string; + +type HotKeyErrorMessages = { + [K in AllowedHotkeyFunc]: string | null; +}; + +type HotKeyLoading = { + [K in AllowedHotkeyFunc]: boolean; +}; + +type HotkeyMap = { [K in AllowedHotkeyFunc]: Key[] }; export default function HotkeyDialog({ open, @@ -31,19 +46,19 @@ export default function HotkeyDialog({ }: HotkeyDialogProps) { const { t } = useTranslation(); - const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu(); - - const [hotkeyMap, setHotkeyMap] = useState>({}); - const hotkeyMapRef = useRef>({}); // 检查是否有快捷键重复 const [duplicateItems, setDuplicateItems] = useState([]); + const { nyanpasuConfig, setNyanpasuConfig } = useNyanpasu(); + + const [hotkeyMap, setHotkeyMap] = useState({} as HotkeyMap); + useEffect(() => { - if (open) { + if (open && Object.keys(hotkeyMap).length === 0) { const map = {} as typeof hotkeyMap; nyanpasuConfig?.hotkeys?.forEach((text) => { const [func, key] = text.split(",").map((i) => i.trim()); if (!func || !key) return; - map[func] = key + map[func as AllowedHotkeyFunc] = key .split("+") .map((e) => e.trim()) .map((k) => (k === "PLUS" ? "+" : k)); @@ -51,61 +66,76 @@ export default function HotkeyDialog({ setHotkeyMap(map); setDuplicateItems([]); } - }, [nyanpasuConfig?.hotkeys, open]); - const isDuplicated = useMemo(() => !!duplicateItems.length, [duplicateItems]); + }, [hotkeyMap, nyanpasuConfig?.hotkeys, open]); - const onBlurCb = useMemoizedFn( - (e: React.FocusEvent, func: string) => { - console.log(func); - const keys = Object.values(hotkeyMapRef.current).flat().filter(Boolean); - const set = new Set(keys); - if (keys.length !== set.size) { - setDuplicateItems([...duplicateItems, func]); - } else { - setDuplicateItems(duplicateItems.filter((e) => e !== func)); + const [errorMessages, setErrorMessages] = useState( + HOTKEY_FUNC.reduce( + (acc, cur) => ({ ...acc, [cur]: null }), + {} as HotKeyErrorMessages, + ), + ); + + const [loading, setLoading] = useState( + HOTKEY_FUNC.reduce( + (acc, cur) => ({ ...acc, [cur]: false }), + {} as HotKeyLoading, + ), + ); + + const saveState = useLockFn( + async (func: AllowedHotkeyFunc, hotkeyMap: HotkeyMap) => { + const hotkeys = Object.entries(hotkeyMap) + .map(([func, keys]) => { + if (!func || !keys?.length) return ""; + + const key = keys + .map((k) => k.trim()) + .filter(Boolean) + .map((k) => (k === "+" ? "PLUS" : k)) + .join("+"); + + if (!key) return ""; + return `${func},${key}`; + }) + .filter(Boolean); + + try { + await setNyanpasuConfig({ hotkeys }); + } catch (err: unknown) { + setErrorMessages((prev) => ({ + ...prev, + [func]: formatError(err), + })); + await message(formatError(err), { + kind: "error", + }); } }, ); - const saveState = useLockFn(async () => { - const hotkeys = Object.entries(hotkeyMap) - .map(([func, keys]) => { - if (!func || !keys?.length) return ""; + const onBlurCb = useCallback( + (e: React.FocusEvent, func: string) => { + const keys = Object.values(hotkeyMap).flat().filter(Boolean); + const set = new Set(keys); + if (keys.length !== set.size) { + setDuplicateItems([...duplicateItems, func]); + return; + } else { + setDuplicateItems(duplicateItems.filter((e) => e !== func)); + } - const key = keys - .map((k) => k.trim()) - .filter(Boolean) - .map((k) => (k === "+" ? "PLUS" : k)) - .join("+"); + setLoading((prev) => ({ ...prev, [func]: true })); - if (!key) return ""; - return `${func},${key}`; - }) - .filter(Boolean); - - try { - await setNyanpasuConfig({ hotkeys }); - } catch (err: any) { - notification({ - title: t("Error"), - body: err.message || err.toString(), - type: NotificationType.Error, - }); - } - }); - - // 自动保存 - useEffect(() => { - if (!isDuplicated && open) { - saveState(); - } - }, [hotkeyMap, isDuplicated, open, saveState]); - - const onSave = () => { - saveState().then(() => { - onClose?.(); - }); - }; + saveState(func as AllowedHotkeyFunc, hotkeyMap) + .catch(() => { + setDuplicateItems([...duplicateItems, func]); + }) + .finally(() => { + setLoading((prev) => ({ ...prev, [func]: false })); + }); + }, + [duplicateItems, hotkeyMap, saveState], + ); return ( {t(func)} { - const map = { ...hotkeyMapRef.current, [func]: v }; - hotkeyMapRef.current = map; - setHotkeyMap(map); - }} + onValueChange={(v) => + setHotkeyMap((prev) => ({ ...prev, [func]: v })) + } /> ))} diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-input.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-input.tsx index 23973fa2ac..ca9d25c864 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-input.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/setting/modules/hotkey-input.tsx @@ -1,9 +1,8 @@ import { parseHotkey } from "@/utils/parse-hotkey"; -import { DeleteRounded } from "@mui/icons-material"; -import { alpha, IconButton, useTheme } from "@mui/material"; +import { Dangerous, DeleteRounded } from "@mui/icons-material"; +import { alpha, CircularProgress, IconButton, useTheme } from "@mui/material"; import type {} from "@mui/material/themeCssVarsAugmentation"; -import clsx from "clsx"; -import { CSSProperties, useRef, useState } from "react"; +import { CSSProperties, useEffect, useRef, useState } from "react"; import { cn, Kbd } from "@nyanpasu/ui"; import styles from "./hotkey-input.module.scss"; @@ -13,23 +12,33 @@ export interface Props extends React.HTMLAttributes { onValueChange?: (value: string[]) => void; func: string; onBlurCb?: (e: React.FocusEvent, func: string) => void; + loading?: boolean; } export default function HotkeyInput({ isDuplicate = false, value, - children, func, onValueChange, onBlurCb, // native className, + loading, ...rest }: Props) { const theme = useTheme(); const changeRef = useRef([]); const [keys, setKeys] = useState(value || []); + const [isClearing, setIsClearing] = useState(false); + + useEffect(() => { + if (isClearing) { + onBlurCb?.({} as React.FocusEvent, func); + setIsClearing(false); + } + }, [func, isClearing, onBlurCb]); + return (
    @@ -84,6 +93,19 @@ export default function HotkeyInput({ {key} ))} + {loading && ( + + )} + {isDuplicate && ( + ({ + color: theme.palette.error.main, + }), + ]} + /> + )}
    @@ -94,7 +116,7 @@ export default function HotkeyInput({ onClick={() => { onValueChange?.([]); setKeys([]); - onBlurCb?.({} as any, func); + setIsClearing(true); }} > diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx index e2b7825c66..d499fba425 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx @@ -1,7 +1,8 @@ import MdiTextBoxCheckOutline from "~icons/mdi/text-box-check-outline"; +import { useLockFn } from "ahooks"; import { AnimatePresence, motion } from "framer-motion"; import { useAtom } from "jotai"; -import { useMemo, useState } from "react"; +import { useMemo, useState, useTransition } from "react"; import { useTranslation } from "react-i18next"; import { useWindowSize } from "react-use"; import { z } from "zod"; @@ -16,14 +17,17 @@ import { } from "@/components/profiles/profile-dialog"; import ProfileItem from "@/components/profiles/profile-item"; import ProfileSide from "@/components/profiles/profile-side"; +import { GlobalUpdatePendingContext } from "@/components/profiles/provider"; import { QuickImport } from "@/components/profiles/quick-import"; import RuntimeConfigDiffDialog from "@/components/profiles/runtime-config-diff-dialog"; import { filterProfiles } from "@/components/profiles/utils"; -import { Public } from "@mui/icons-material"; -import { Badge, Button, IconButton } from "@mui/material"; +import { formatError } from "@/utils"; +import { message } from "@/utils/notification"; +import { Public, Update } from "@mui/icons-material"; +import { Badge, Button, CircularProgress, IconButton } from "@mui/material"; import Grid from "@mui/material/Grid2"; -import { Profile, useClash } from "@nyanpasu/interface"; -import { SidePage } from "@nyanpasu/ui"; +import { Profile, updateProfile, useClash } from "@nyanpasu/interface"; +import { FloatingButton, SidePage } from "@nyanpasu/ui"; import { createFileRoute, useLocation } from "@tanstack/react-router"; import { zodSearchValidator } from "@tanstack/router-zod-adapter"; @@ -119,6 +123,30 @@ function ProfilePage() { const { width } = useWindowSize(); + const [globalUpdatePending, startGlobalUpdate] = useTransition(); + const handleGlobalProfileUpdate = useLockFn(async () => { + await startGlobalUpdate(async () => { + const remoteProfiles = + profiles?.filter((item) => item.type == "remote") || []; + const updates: Array> = []; + for (const profile of remoteProfiles) { + const options: Profile.Option = profile.option || { + with_proxy: false, + self_proxy: false, + }; + + updates.push(updateProfile(profile.uid, options)); + } + try { + await Promise.all(updates); + } catch (e) { + message(`failed to update profiles: \n${formatError(e)}`, { + kind: "error", + }); + } + }); + }); + return ( } > -
    - + +
    + - {profiles && ( - - {profiles.map((item) => ( - - + {profiles.map((item) => ( + - - - - ))} - - )} -
    + + + + + ))} + + )} +
    +
    - +
    + ({ + backgroundColor: theme.palette.grey[200], + boxShadow: 4, + "&:hover": { + backgroundColor: theme.palette.grey[300], + }, + }), + ]} + onClick={handleGlobalProfileUpdate} + > + {globalUpdatePending ? : } + + +
    ); diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index 36fc261c56..5e94211a8f 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -34,7 +34,7 @@ "react-error-boundary": "4.0.13", "react-i18next": "15.0.2", "react-use": "17.5.1", - "vite": "5.4.6", + "vite": "5.4.7", "vite-tsconfig-paths": "5.0.1" }, "devDependencies": { @@ -42,7 +42,7 @@ "@types/d3-interpolate-path": "2.0.3", "clsx": "2.1.1", "d3-interpolate-path": "2.3.0", - "sass": "1.79.2", + "sass": "1.79.3", "tailwind-merge": "2.5.2", "typescript-plugin-css-modules": "5.1.0", "vite-plugin-dts": "4.2.1" diff --git a/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx b/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx index b268b6eed9..4027bf3160 100644 --- a/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx +++ b/clash-nyanpasu/frontend/ui/src/materialYou/components/baseDialog/index.tsx @@ -60,6 +60,7 @@ export const BaseDialog = ({ }); const [okLoading, setOkLoading] = useState(false); + const [closeLoading, setCloseLoading] = useState(false); const { run: runMounted, cancel: cancelMounted } = useDebounceFn( () => setMounted(false), @@ -79,12 +80,22 @@ export const BaseDialog = ({ } }, [open]); - const handleClose = useCallback(() => { + const handleClose = useLockFn(async () => { if (onClose) { - onClose(); - runMounted(); + if (onClose.constructor.name === "AsyncFunction") { + try { + setCloseLoading(true); + + await onClose(); + } finally { + setCloseLoading(false); + } + } else { + onClose(); + } } - }, [onClose, runMounted]); + runMounted(); + }); const handleOk = useLockFn(async () => { if (!onOk) return; @@ -208,9 +219,14 @@ export const BaseDialog = ({
    {onClose && ( - + )} {onOk && ( diff --git a/clash-nyanpasu/frontend/ui/src/materialYou/components/floatingButton/index.tsx b/clash-nyanpasu/frontend/ui/src/materialYou/components/floatingButton/index.tsx index 0a316d8601..fba61ac990 100644 --- a/clash-nyanpasu/frontend/ui/src/materialYou/components/floatingButton/index.tsx +++ b/clash-nyanpasu/frontend/ui/src/materialYou/components/floatingButton/index.tsx @@ -1,4 +1,5 @@ import { ReactNode } from "react"; +import { cn } from "@/utils"; import { alpha, Button, ButtonProps, useTheme } from "@mui/material"; export interface FloatingButtonProps extends ButtonProps { @@ -15,7 +16,10 @@ export const FloatingButton = ({ return (