diff --git a/.github/update.log b/.github/update.log index 33fd48a825..1c21a5509c 100644 --- a/.github/update.log +++ b/.github/update.log @@ -954,3 +954,4 @@ Update On Sun Mar 23 19:33:38 CET 2025 Update On Mon Mar 24 19:37:20 CET 2025 Update On Tue Mar 25 19:35:52 CET 2025 Update On Wed Mar 26 19:37:44 CET 2025 +Update On Thu Mar 27 19:36:04 CET 2025 diff --git a/clash-meta/listener/anytls/server.go b/clash-meta/listener/anytls/server.go index 31a7c55ae9..aa8d946a6f 100644 --- a/clash-meta/listener/anytls/server.go +++ b/clash-meta/listener/anytls/server.go @@ -174,6 +174,12 @@ func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) { return } + // It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly. + err = stream.HandshakeSuccess() + if err != nil { + return + } + h.NewConnection(ctx, stream, M.Metadata{ Source: M.SocksaddrFromNet(conn.RemoteAddr()), Destination: destination, diff --git a/clash-meta/transport/anytls/padding/padding.go b/clash-meta/transport/anytls/padding/padding.go index addd47c2fb..498feb05c0 100644 --- a/clash-meta/transport/anytls/padding/padding.go +++ b/clash-meta/transport/anytls/padding/padding.go @@ -15,10 +15,10 @@ import ( const CheckMark = -1 var DefaultPaddingScheme = []byte(`stop=8 -0=34-120 +0=30-30 1=100-400 -2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 -3=500-1000 +2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 +3=9-9,500-1000 4=500-1000 5=500-1000 6=500-1000 diff --git a/clash-meta/transport/anytls/session/client.go b/clash-meta/transport/anytls/session/client.go index 5e99f13562..50fd7b42ef 100644 --- a/clash-meta/transport/anytls/session/client.go +++ b/clash-meta/transport/anytls/session/client.go @@ -83,11 +83,7 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { } stream.dieHook = func() { - if session.IsClosed() { - if session.dieHook != nil { - session.dieHook() - } - } else { + if !session.IsClosed() { select { case <-c.die.Done(): // Now client has been closed @@ -154,10 +150,10 @@ func (c *Client) Close() error { c.sessionsLock.Lock() sessionToClose := make([]*Session, 0, len(c.sessions)) - for seq, session := range c.sessions { + for _, session := range c.sessions { sessionToClose = append(sessionToClose, session) - delete(c.sessions, seq) } + c.sessions = make(map[uint64]*Session) c.sessionsLock.Unlock() for _, session := range sessionToClose { diff --git a/clash-meta/transport/anytls/session/frame.go b/clash-meta/transport/anytls/session/frame.go index 49597c5526..8f6283213d 100644 --- a/clash-meta/transport/anytls/session/frame.go +++ b/clash-meta/transport/anytls/session/frame.go @@ -9,9 +9,14 @@ const ( // cmds cmdSYN = 1 // stream open cmdPSH = 2 // data push cmdFIN = 3 // stream close, a.k.a EOF mark - cmdSettings = 4 // Settings + cmdSettings = 4 // Settings (Client send to Server) cmdAlert = 5 // Alert cmdUpdatePaddingScheme = 6 // update padding scheme + // Since version 2 + cmdSYNACK = 7 // Server reports to the client that the stream has been opened + cmdHeartRequest = 8 // Keep alive command + cmdHeartResponse = 9 // Keep alive command + cmdServerSettings = 10 // Settings (Server send to client) ) const ( diff --git a/clash-meta/transport/anytls/session/session.go b/clash-meta/transport/anytls/session/session.go index 963533ead1..c80639ce3b 100644 --- a/clash-meta/transport/anytls/session/session.go +++ b/clash-meta/transport/anytls/session/session.go @@ -3,9 +3,11 @@ package session import ( "crypto/md5" "encoding/binary" + "fmt" "io" "net" "runtime/debug" + "strconv" "sync" "time" @@ -30,11 +32,16 @@ type Session struct { die chan struct{} dieHook func() + synDone func() + synDoneLock sync.Mutex + // pool seq uint64 idleSince time.Time padding *atomic.TypedValue[*padding.PaddingFactory] + peerVersion byte + // client isClient bool sendPadding bool @@ -76,7 +83,7 @@ func (s *Session) Run() { } settings := util.StringMap{ - "v": "1", + "v": "2", "client": "mihomo/" + constant.Version, "padding-md5": s.padding.Load().Md5, } @@ -105,15 +112,16 @@ func (s *Session) Close() error { close(s.die) once = true }) - if once { if s.dieHook != nil { s.dieHook() + s.dieHook = nil } s.streamLock.Lock() - for k := range s.streams { - s.streams[k].sessionClose() + for _, stream := range s.streams { + stream.Close() } + s.streams = make(map[uint32]*Stream) s.streamLock.Unlock() return s.conn.Close() } else { @@ -132,6 +140,17 @@ func (s *Session) OpenStream() (*Stream, error) { //logrus.Debugln("stream open", sid, s.streams) + if sid >= 2 && s.peerVersion >= 2 { + s.synDoneLock.Lock() + if s.synDone != nil { + s.synDone() + } + s.synDone = util.NewDeadlineWatcher(time.Second*3, func() { + s.Close() + }) + s.synDoneLock.Unlock() + } + if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { return nil, err } @@ -195,13 +214,37 @@ func (s *Session) recvLoop() error { if _, ok := s.streams[sid]; !ok { stream := newStream(sid, s) s.streams[sid] = stream - if s.onNewStream != nil { - go s.onNewStream(stream) - } else { - go s.Close() - } + go func() { + if s.onNewStream != nil { + s.onNewStream(stream) + } else { + stream.Close() + } + }() } s.streamLock.Unlock() + case cmdSYNACK: // should be client only + s.synDoneLock.Lock() + if s.synDone != nil { + s.synDone() + s.synDone = nil + } + s.synDoneLock.Unlock() + if hdr.Length() > 0 { + buffer := pool.Get(int(hdr.Length())) + if _, err := io.ReadFull(s.conn, buffer); err != nil { + pool.Put(buffer) + return err + } + // report error + s.streamLock.RLock() + stream, ok := s.streams[sid] + s.streamLock.RUnlock() + if ok { + stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer))) + } + pool.Put(buffer) + } case cmdFIN: s.streamLock.RLock() stream, ok := s.streams[sid] @@ -240,6 +283,20 @@ func (s *Session) recvLoop() error { return err } } + // check client's version + if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 { + s.peerVersion = byte(v) + // send cmdServerSettings + f := newFrame(cmdServerSettings, 0) + f.data = util.StringMap{ + "v": "2", + }.ToBytes() + _, err = s.writeFrame(f) + if err != nil { + pool.Put(buffer) + return err + } + } } pool.Put(buffer) } @@ -265,12 +322,35 @@ func (s *Session) recvLoop() error { } if s.isClient { if padding.UpdatePaddingScheme(rawScheme, s.padding) { - log.Infoln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) + log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) } else { log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) } } } + case cmdHeartRequest: + if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil { + return err + } + case cmdHeartResponse: + // Active keepalive checking is not implemented yet + break + case cmdServerSettings: + if hdr.Length() > 0 { + buffer := pool.Get(int(hdr.Length())) + if _, err := io.ReadFull(s.conn, buffer); err != nil { + pool.Put(buffer) + return err + } + if s.isClient { + // check server's version + m := util.StringMapFromBytes(buffer) + if v, err := strconv.Atoi(m["v"]); err == nil { + s.peerVersion = byte(v) + } + } + pool.Put(buffer) + } default: // I don't know what command it is (can't have data) } @@ -280,8 +360,10 @@ func (s *Session) recvLoop() error { } } -// notify the session that a stream has closed func (s *Session) streamClosed(sid uint32) error { + if s.IsClosed() { + return io.ErrClosedPipe + } _, err := s.writeFrame(newFrame(cmdFIN, sid)) s.streamLock.Lock() delete(s.streams, sid) diff --git a/clash-meta/transport/anytls/session/stream.go b/clash-meta/transport/anytls/session/stream.go index 9f21ff04b3..1ec8fbc9be 100644 --- a/clash-meta/transport/anytls/session/stream.go +++ b/clash-meta/transport/anytls/session/stream.go @@ -22,6 +22,9 @@ type Stream struct { dieOnce sync.Once dieHook func() + dieErr error + + reportOnce sync.Once } // newStream initiates a Stream struct @@ -36,7 +39,11 @@ func newStream(id uint32, sess *Session) *Stream { // Read implements net.Conn func (s *Stream) Read(b []byte) (n int, err error) { - return s.pipeR.Read(b) + n, err = s.pipeR.Read(b) + if s.dieErr != nil { + err = s.dieErr + } + return } // Write implements net.Conn @@ -54,25 +61,28 @@ func (s *Stream) Write(b []byte) (n int, err error) { // Close implements net.Conn func (s *Stream) Close() error { - if s.sessionClose() { - // notify remote - return s.sess.streamClosed(s.id) - } else { - return io.ErrClosedPipe - } + return s.CloseWithError(io.ErrClosedPipe) } -// sessionClose close stream from session side, do not notify remote -func (s *Stream) sessionClose() (once bool) { +func (s *Stream) CloseWithError(err error) error { + // if err != io.ErrClosedPipe { + // logrus.Debugln(err) + // } + var once bool s.dieOnce.Do(func() { + s.dieErr = err s.pipeR.Close() once = true + }) + if once { if s.dieHook != nil { s.dieHook() s.dieHook = nil } - }) - return + return s.sess.streamClosed(s.id) + } else { + return s.dieErr + } } func (s *Stream) SetReadDeadline(t time.Time) error { @@ -108,3 +118,33 @@ func (s *Stream) RemoteAddr() net.Addr { } return nil } + +// HandshakeFailure should be called when Server fail to create outbound proxy +func (s *Stream) HandshakeFailure(err error) error { + var once bool + s.reportOnce.Do(func() { + once = true + }) + if once && err != nil && s.sess.peerVersion >= 2 { + f := newFrame(cmdSYNACK, s.id) + f.data = []byte(err.Error()) + if _, err := s.sess.writeFrame(f); err != nil { + return err + } + } + return nil +} + +// HandshakeSuccess should be called when Server success to create outbound proxy +func (s *Stream) HandshakeSuccess() error { + var once bool + s.reportOnce.Do(func() { + once = true + }) + if once && s.sess.peerVersion >= 2 { + if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil { + return err + } + } + return nil +} diff --git a/clash-meta/transport/anytls/util/deadline.go b/clash-meta/transport/anytls/util/deadline.go new file mode 100644 index 0000000000..8167bf9555 --- /dev/null +++ b/clash-meta/transport/anytls/util/deadline.go @@ -0,0 +1,25 @@ +package util + +import ( + "sync" + "time" +) + +func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) { + t := time.NewTimer(ddl) + closeCh := make(chan struct{}) + go func() { + defer t.Stop() + select { + case <-closeCh: + case <-t.C: + timeOut() + } + }() + var once sync.Once + return func() { + once.Do(func() { + close(closeCh) + }) + } +} diff --git a/clash-nyanpasu/.github/workflows/daily.yml b/clash-nyanpasu/.github/workflows/daily.yml index df7ceb117b..f6a6f109b2 100644 --- a/clash-nyanpasu/.github/workflows/daily.yml +++ b/clash-nyanpasu/.github/workflows/daily.yml @@ -16,7 +16,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - uses: pnpm/action-setup@v4 name: Install pnpm @@ -57,7 +57,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-nyanpasu/.github/workflows/macos-aarch64.yaml b/clash-nyanpasu/.github/workflows/macos-aarch64.yaml index f7fd97e830..6a83963430 100644 --- a/clash-nyanpasu/.github/workflows/macos-aarch64.yaml +++ b/clash-nyanpasu/.github/workflows/macos-aarch64.yaml @@ -34,7 +34,7 @@ jobs: - name: Install Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - uses: pnpm/action-setup@v4 name: Install pnpm diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 98b652c62d..7474b6f9c7 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -53,7 +53,7 @@ "@csstools/normalize.css": "12.1.1", "@emotion/babel-plugin": "11.13.5", "@emotion/react": "11.14.0", - "@iconify/json": "2.2.319", + "@iconify/json": "2.2.321", "@monaco-editor/react": "4.7.0", "@tanstack/react-query": "5.69.0", "@tanstack/react-router": "1.114.27", diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 1817987304..796662a99e 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -5,7 +5,7 @@ "mihomo_alpha": "alpha-7b38261", "clash_rs": "v0.7.6", "clash_premium": "2023-09-05-gdcc8d87", - "clash_rs_alpha": "0.7.6-alpha+sha.153fb70" + "clash_rs_alpha": "0.7.6-alpha+sha.fa04093" }, "arch_template": { "mihomo": { @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-03-24T22:21:04.112Z" + "updated_at": "2025-03-26T22:20:53.137Z" } diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 3d8f2f72d9..098208f54b 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -333,8 +333,8 @@ importers: specifier: 11.14.0 version: 11.14.0(@types/react@19.0.12)(react@19.0.0) '@iconify/json': - specifier: 2.2.319 - version: 2.2.319 + specifier: 2.2.321 + version: 2.2.321 '@monaco-editor/react': specifier: 4.7.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1674,8 +1674,8 @@ packages: '@vue/compiler-sfc': optional: true - '@iconify/json@2.2.319': - resolution: {integrity: sha512-ZGX8O3PXDxXdgltuW2JlXa7IuZ6uc34qKVIBRyPNo63fxjbw7rSOox7HKi3fJyhXqoL3aN0AtK1yOb5Cgcte8w==} + '@iconify/json@2.2.321': + resolution: {integrity: sha512-0D1OjRK77jD7dhrb4IhGiBTqLufi6I6HaYso6qkSkvm0WqbWgzGnoNEpw+g/jzSJAiLfuBwOGz6b7Q/ZJqsYrw==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -9450,7 +9450,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@iconify/json@2.2.319': + '@iconify/json@2.2.321': dependencies: '@iconify/types': 2.0.0 pathe: 1.1.2 diff --git a/clash-verge-rev/.husky/pre-push b/clash-verge-rev/.husky/pre-push index 807ac5febc..c90d45ef17 100644 --- a/clash-verge-rev/.husky/pre-push +++ b/clash-verge-rev/.husky/pre-push @@ -1,13 +1,13 @@ #!/bin/bash # 运行 clippy -cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix +# cargo clippy --manifest-path ./src-tauri/Cargo.toml --fix # 如果 clippy 失败,阻止 push -if [ $? -ne 0 ]; then - echo "Clippy found issues in sub_crate. Please fix them before pushing." - exit 1 -fi +# if [ $? -ne 0 ]; then +# echo "Clippy found issues in sub_crate. Please fix them before pushing." +# exit 1 +# fi # 允许 push exit 0 diff --git a/clash-verge-rev/UPDATELOG.md b/clash-verge-rev/UPDATELOG.md index d7208e1924..379b968bc8 100644 --- a/clash-verge-rev/UPDATELOG.md +++ b/clash-verge-rev/UPDATELOG.md @@ -2,14 +2,24 @@ #### 已知问题 - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 + - MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙 ### 2.2.3-alpha 相对于 2.2.2 #### 修复了: - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题 + - “开启自启”和“DNS覆写”开关跳动问题 + - 自定义托盘图标未能应用更改 + - MacOS 自定义托盘图标显示速率时图标和文本间隙过大 + - MacOS 托盘速率显示不全 -#### 优化 - - 重构了内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 - - 集中管理应用数据,优化数据获取和刷新逻辑 +#### 新增了: + - ClashVergeRev 从现在开始不再强依赖系统服务和管理权限 + +#### 优化了: + - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 + - 前端统一刷新应用数据,优化数据获取和刷新逻辑 + - 优化首页流量图表代码,调整图表文字边距 + - MacOS 托盘速率更好的显示样式和更新逻辑 ## v2.2.2 diff --git a/clash-verge-rev/src-tauri/Cargo.lock b/clash-verge-rev/src-tauri/Cargo.lock index 16e0c0a03a..f774c803be 100644 --- a/clash-verge-rev/src-tauri/Cargo.lock +++ b/clash-verge-rev/src-tauri/Cargo.lock @@ -276,17 +276,6 @@ dependencies = [ "futures-lite 1.13.0", ] -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock 3.4.0", - "blocking", - "futures-lite 2.6.0", -] - [[package]] name = "async-io" version = "1.13.0" @@ -374,25 +363,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "async-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" -dependencies = [ - "async-channel 2.3.1", - "async-io 2.4.0", - "async-lock 3.4.0", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.3.0", - "futures-lite 2.6.0", - "rustix 0.38.44", - "tracing", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -1118,7 +1088,6 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-global-shortcut", - "tauri-plugin-notification", "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", @@ -1761,16 +1730,6 @@ dependencies = [ "dirs-sys 0.5.0", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -1794,17 +1753,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -3795,19 +3743,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mac-notification-sys" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce8f34f3717aa37177e723df6c1fc5fb02b2a1087374ea3fe0ea42316dc8f91" -dependencies = [ - "cc", - "dirs-next", - "objc-foundation", - "objc_id", - "time", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -4195,20 +4130,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "notify-rust" -version = "4.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa3b9f2364a09bd359aa0206702882e208437450866a374d5372d64aece4029" -dependencies = [ - "futures-lite 2.6.0", - "log", - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - [[package]] name = "ntapi" version = "0.4.1" @@ -4370,17 +4291,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -4645,15 +4555,6 @@ dependencies = [ "objc2-foundation 0.3.0", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.7" @@ -5411,15 +5312,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.32.0" @@ -6570,11 +6462,11 @@ checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" dependencies = [ "async-channel 1.9.0", "async-executor", - "async-fs 1.6.0", + "async-fs", "async-io 1.13.0", "async-lock 2.8.0", "async-net", - "async-process 1.8.1", + "async-process", "blocking", "futures-lite 1.13.0", ] @@ -7176,25 +7068,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "tauri-plugin-notification" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c474c7cc524385e682ccc1e149e13913a66fd8586ac4c2319cf01b78f070d309" -dependencies = [ - "log", - "notify-rust", - "rand 0.8.5", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "thiserror 2.0.12", - "time", - "url", -] - [[package]] name = "tauri-plugin-process" version = "2.2.0" @@ -7369,17 +7242,6 @@ dependencies = [ "toml", ] -[[package]] -name = "tauri-winrt-notification" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871" -dependencies = [ - "quick-xml 0.31.0", - "windows 0.56.0", - "windows-version", -] - [[package]] name = "tempfile" version = "3.19.1" @@ -8721,16 +8583,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" -dependencies = [ - "windows-core 0.56.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.57.0" @@ -8782,18 +8634,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" -dependencies = [ - "windows-implement 0.56.0", - "windows-interface 0.56.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -8842,17 +8682,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-implement" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "windows-implement" version = "0.57.0" @@ -8886,17 +8715,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "windows-interface" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "windows-interface" version = "0.57.0" @@ -9553,15 +9371,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" dependencies = [ "async-broadcast", - "async-executor", - "async-fs 2.1.2", - "async-io 2.4.0", - "async-lock 3.4.0", - "async-process 2.3.0", "async-recursion", - "async-task", "async-trait", - "blocking", "enumflags2", "event-listener 5.3.0", "futures-core", diff --git a/clash-verge-rev/src-tauri/Cargo.toml b/clash-verge-rev/src-tauri/Cargo.toml index 8d23dbf4fa..b145a18ba5 100755 --- a/clash-verge-rev/src-tauri/Cargo.toml +++ b/clash-verge-rev/src-tauri/Cargo.toml @@ -35,10 +35,10 @@ delay_timer = "0.11.6" parking_lot = "0.12" percent-encoding = "2.3.1" tokio = { version = "1.44", features = [ - "rt-multi-thread", - "macros", - "time", - "sync", + "rt-multi-thread", + "macros", + "time", + "sync", ] } serde = { version = "1.0", features = ["derive"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] } @@ -47,17 +47,16 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d74 image = "0.25.6" imageproc = "0.25.0" tauri = { version = "2.4.0", features = [ - "protocol-asset", - "devtools", - "tray-icon", - "image-ico", - "image-png", + "protocol-asset", + "devtools", + "tray-icon", + "image-ico", + "image-png", ] } network-interface = { version = "2.0.0", features = ["serde"] } tauri-plugin-shell = "2.2.0" tauri-plugin-dialog = "2.2.0" tauri-plugin-fs = "2.2.0" -tauri-plugin-notification = "2.2.2" tauri-plugin-process = "2.2.0" tauri-plugin-clipboard-manager = "2.2.2" tauri-plugin-deep-link = "2.2.0" diff --git a/clash-verge-rev/src-tauri/capabilities/migrated.json b/clash-verge-rev/src-tauri/capabilities/migrated.json index e5920910ff..67dc15cf3e 100644 --- a/clash-verge-rev/src-tauri/capabilities/migrated.json +++ b/clash-verge-rev/src-tauri/capabilities/migrated.json @@ -68,7 +68,6 @@ "shell:allow-spawn", "shell:allow-stdin-write", "dialog:allow-open", - "notification:default", "global-shortcut:allow-is-registered", "global-shortcut:allow-register", "global-shortcut:allow-register-all", @@ -79,7 +78,6 @@ "clipboard-manager:allow-read-text", "clipboard-manager:allow-write-text", "shell:default", - "dialog:default", - "notification:default" + "dialog:default" ] } diff --git a/clash-verge-rev/src-tauri/src/cmd/app.rs b/clash-verge-rev/src-tauri/src/cmd/app.rs index cd54de3ac4..99fb12b8cc 100644 --- a/clash-verge-rev/src-tauri/src/cmd/app.rs +++ b/clash-verge-rev/src-tauri/src/cmd/app.rs @@ -1,5 +1,9 @@ use super::CmdResult; -use crate::{feat, utils::dirs, wrap_err}; +use crate::{ + feat, logging, + utils::{dirs, logging::Type}, + wrap_err, +}; use tauri::Manager; /// 打开应用程序所在目录 @@ -194,7 +198,14 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult { ) .unwrap_or_default(); } - + logging!( + info, + Type::CMD, + true, + "Copying icon file path: {:?} -> file dist: {:?}", + path, + dest_path + ); match fs::copy(file_path, &dest_path) { Ok(_) => Ok(dest_path.to_string_lossy().to_string()), Err(err) => Err(err.to_string()), diff --git a/clash-verge-rev/src-tauri/src/cmd/profile.rs b/clash-verge-rev/src-tauri/src/cmd/profile.rs index 9537ef36d9..0b1c46626a 100644 --- a/clash-verge-rev/src-tauri/src/cmd/profile.rs +++ b/clash-verge-rev/src-tauri/src/cmd/profile.rs @@ -2,8 +2,8 @@ use super::CmdResult; use crate::{ config::*, core::*, - feat, log_err, ret_err, - utils::{dirs, help}, + feat, log_err, logging, ret_err, + utils::{dirs, help, logging::Type}, wrap_err, }; @@ -77,20 +77,19 @@ pub async fn delete_profile(index: String) -> CmdResult { /// 修改profiles的配置 #[tauri::command] pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { - println!("[cmd配置patch] 开始修改配置文件"); + logging!(info, Type::CMD, true, "开始修改配置文件"); // 保存当前配置,以便在验证失败时恢复 let current_profile = Config::profiles().latest().current.clone(); - println!("[cmd配置patch] 当前配置: {:?}", current_profile); + logging!(info, Type::CMD, true, "当前配置: {:?}", current_profile); // 更新profiles配置 - println!("[cmd配置patch] 正在更新配置草稿"); + logging!(info, Type::CMD, true, "正在更新配置草稿"); wrap_err!({ Config::profiles().draft().patch_config(profiles) })?; - // 更新配置并进行验证 match CoreManager::global().update_config().await { Ok((true, _)) => { - println!("[cmd配置patch] 配置更新成功"); + logging!(info, Type::CMD, true, "配置更新成功"); handle::Handle::refresh_clash(); let _ = tray::Tray::global().update_tooltip(); Config::profiles().apply(); @@ -98,12 +97,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { Ok(true) } Ok((false, error_msg)) => { - println!("[cmd配置patch] 配置验证失败: {}", error_msg); + logging!(warn, Type::CMD, true, "配置验证失败: {}", error_msg); Config::profiles().discard(); - // 如果验证失败,恢复到之前的配置 if let Some(prev_profile) = current_profile { - println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile); + logging!( + info, + Type::CMD, + true, + "尝试恢复到之前的配置: {}", + prev_profile + ); let restore_profiles = IProfiles { current: Some(prev_profile), items: None, @@ -112,7 +116,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?; Config::profiles().apply(); wrap_err!(Config::profiles().data().save_file())?; - println!("[cmd配置patch] 成功恢复到之前的配置"); + logging!(info, Type::CMD, true, "成功恢复到之前的配置"); } // 发送验证错误通知 @@ -120,7 +124,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { Ok(false) } Err(e) => { - println!("[cmd配置patch] 更新过程发生错误: {}", e); + logging!(warn, Type::CMD, true, "更新过程发生错误: {}", e); Config::profiles().discard(); handle::Handle::notice_message("config_validate::boot_error", e.to_string()); Ok(false) diff --git a/clash-verge-rev/src-tauri/src/cmd/proxy.rs b/clash-verge-rev/src-tauri/src/cmd/proxy.rs index a196000adf..cee4c68b03 100644 --- a/clash-verge-rev/src-tauri/src/cmd/proxy.rs +++ b/clash-verge-rev/src-tauri/src/cmd/proxy.rs @@ -4,21 +4,21 @@ use crate::module::mihomo::MihomoManager; #[tauri::command] pub async fn get_proxies() -> CmdResult { let mannager = MihomoManager::global(); - let proxies = mannager + + mannager .refresh_proxies() .await .map(|_| mannager.get_proxies()) - .or_else(|_| Ok(mannager.get_proxies())); - proxies + .or_else(|_| Ok(mannager.get_proxies())) } #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { let mannager = MihomoManager::global(); - let providers = mannager + + mannager .refresh_providers_proxies() .await .map(|_| mannager.get_providers_proxies()) - .or_else(|_| Ok(mannager.get_providers_proxies())); - providers + .or_else(|_| Ok(mannager.get_providers_proxies())) } diff --git a/clash-verge-rev/src-tauri/src/core/core.rs b/clash-verge-rev/src-tauri/src/core/core.rs index 256c62b3c4..52be80d555 100644 --- a/clash-verge-rev/src-tauri/src/core/core.rs +++ b/clash-verge-rev/src-tauri/src/core/core.rs @@ -4,9 +4,9 @@ use crate::{ config::*, core::{ handle, - service::{self, is_service_available}, + service::{self}, }, - log_err, logging, logging_error, + logging, logging_error, module::mihomo::MihomoManager, utils::{ dirs, @@ -63,7 +63,14 @@ impl CoreManager { let content = match std::fs::read_to_string(path) { Ok(content) => content, Err(err) => { - log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err); + logging!( + warn, + Type::Config, + true, + "无法读取文件以检测类型: {}, 错误: {}", + path, + err + ); return Err(anyhow::anyhow!( "Failed to read file to detect type: {}", err @@ -114,7 +121,13 @@ impl CoreManager { } // 默认情况:无法确定时,假设为非脚本文件(更安全) - log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path); + logging!( + debug, + Type::Config, + true, + "无法确定文件类型,默认当作YAML处理: {}", + path + ); Ok(false) } /// 使用默认配置 @@ -147,7 +160,7 @@ impl CoreManager { ) -> Result<(bool, String)> { // 检查程序是否正在退出,如果是则跳过验证 if handle::Handle::global().is_exiting() { - println!("[core配置验证] 应用正在退出,跳过验证"); + logging!(info, Type::Core, true, "应用正在退出,跳过验证"); return Ok((true, String::new())); } @@ -160,8 +173,11 @@ impl CoreManager { // 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证 if is_merge_file.unwrap_or(false) { - println!( - "[core配置验证] 检测到Merge文件,仅进行语法检查: {}", + logging!( + info, + Type::Config, + true, + "检测到Merge文件,仅进行语法检查: {}", config_path ); return self.validate_file_syntax(config_path).await; @@ -175,19 +191,38 @@ impl CoreManager { Ok(result) => result, Err(err) => { // 如果无法确定文件类型,尝试使用Clash内核验证 - log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err); + logging!( + warn, + Type::Config, + true, + "无法确定文件类型: {}, 错误: {}", + config_path, + err + ); return self.validate_config_internal(config_path).await; } } }; if is_script { - log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path); + logging!( + info, + Type::Config, + true, + "检测到脚本文件,使用JavaScript验证: {}", + config_path + ); return self.validate_script_file(config_path).await; } // 对YAML配置文件使用Clash内核验证 - log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path); + logging!( + info, + Type::Config, + true, + "使用Clash内核验证配置文件: {}", + config_path + ); self.validate_config_internal(config_path).await } /// 内部验证配置文件的实现 @@ -234,7 +269,7 @@ impl CoreManager { logging!(info, Type::Config, true, "-------- 验证结果 --------"); if !stderr.is_empty() { - logging!(info, Type::Core, true, "stderr输出:\n{}", stderr); + logging!(info, Type::Config, true, "stderr输出:\n{}", stderr); } if has_error { @@ -259,29 +294,28 @@ impl CoreManager { } /// 只进行文件语法检查,不进行完整验证 async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> { - println!("[core配置语法检查] 开始检查文件: {}", config_path); + logging!(info, Type::Config, true, "开始检查文件: {}", config_path); // 读取文件内容 let content = match std::fs::read_to_string(config_path) { Ok(content) => content, Err(err) => { let error_msg = format!("Failed to read file: {}", err); - println!("[core配置语法检查] 无法读取文件: {}", error_msg); + logging!(error, Type::Config, true, "无法读取文件: {}", error_msg); return Ok((false, error_msg)); } }; - // 对YAML文件尝试解析,只检查语法正确性 - println!("[core配置语法检查] 进行YAML语法检查"); + logging!(info, Type::Config, true, "进行YAML语法检查"); match serde_yaml::from_str::(&content) { Ok(_) => { - println!("[core配置语法检查] YAML语法检查通过"); + logging!(info, Type::Config, true, "YAML语法检查通过"); Ok((true, String::new())) } Err(err) => { // 使用标准化的前缀,以便错误处理函数能正确识别 let error_msg = format!("YAML syntax error: {}", err); - println!("[core配置语法检查] YAML语法错误: {}", error_msg); + logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg); Ok((false, error_msg)) } } @@ -293,13 +327,13 @@ impl CoreManager { Ok(content) => content, Err(err) => { let error_msg = format!("Failed to read script file: {}", err); - log::warn!(target: "app", "脚本语法错误: {}", err); + logging!(warn, Type::Config, true, "脚本语法错误: {}", err); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); return Ok((false, error_msg)); } }; - log::debug!(target: "app", "验证脚本文件: {}", path); + logging!(debug, Type::Config, true, "验证脚本文件: {}", path); // 使用boa引擎进行基本语法检查 use boa_engine::{Context, Source}; @@ -309,7 +343,7 @@ impl CoreManager { match result { Ok(_) => { - log::debug!(target: "app", "脚本语法验证通过: {}", path); + logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path); // 检查脚本是否包含main函数 if !content.contains("function main") @@ -317,7 +351,7 @@ impl CoreManager { && !content.contains("let main") { let error_msg = "Script must contain a main function"; - log::warn!(target: "app", "脚本缺少main函数: {}", path); + logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path); //handle::Handle::notice_message("config_validate::script_missing_main", error_msg); return Ok((false, error_msg.to_string())); } @@ -326,7 +360,7 @@ impl CoreManager { } Err(err) => { let error_msg = format!("Script syntax error: {}", err); - log::warn!(target: "app", "脚本语法错误: {}", err); + logging!(warn, Type::Config, true, "脚本语法错误: {}", err); //handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg); Ok((false, error_msg)) } @@ -336,39 +370,45 @@ impl CoreManager { pub async fn update_config(&self) -> Result<(bool, String)> { // 检查程序是否正在退出,如果是则跳过完整验证流程 if handle::Handle::global().is_exiting() { - println!("[core配置更新] 应用正在退出,跳过验证"); + logging!(info, Type::Config, true, "应用正在退出,跳过验证"); return Ok((true, String::new())); } - println!("[core配置更新] 开始更新配置"); + logging!(info, Type::Config, true, "开始更新配置"); // 1. 先生成新的配置内容 - println!("[core配置更新] 生成新的配置内容"); + logging!(info, Type::Config, true, "生成新的配置内容"); Config::generate().await?; // 2. 生成临时文件并进行验证 - println!("[core配置更新] 生成临时配置文件用于验证"); + logging!(info, Type::Config, true, "生成临时配置文件用于验证"); let temp_config = Config::generate_file(ConfigType::Check)?; let temp_config = dirs::path_to_str(&temp_config)?; - println!("[core配置更新] 临时配置文件路径: {}", temp_config); + logging!( + info, + Type::Config, + true, + "临时配置文件路径: {}", + temp_config + ); // 3. 验证配置 match self.validate_config().await { Ok((true, _)) => { - println!("[core配置更新] 配置验证通过"); + logging!(info, Type::Config, true, "配置验证通过"); // 4. 验证通过后,生成正式的运行时配置 - println!("[core配置更新] 生成运行时配置"); + logging!(info, Type::Config, true, "生成运行时配置"); let run_path = Config::generate_file(ConfigType::Run)?; - logging_error!(Type::Core, true, self.put_configs_force(run_path).await); + logging_error!(Type::Config, true, self.put_configs_force(run_path).await); Ok((true, "something".into())) } Ok((false, error_msg)) => { - println!("[core配置更新] 配置验证失败: {}", error_msg); + logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg); Config::runtime().discard(); Ok((false, error_msg)) } Err(e) => { - println!("[core配置更新] 验证过程发生错误: {}", e); + logging!(warn, Type::Config, true, "验证过程发生错误: {}", e); Config::runtime().discard(); Err(e) } @@ -480,14 +520,10 @@ impl CoreManager { pub async fn init(&self) -> Result<()> { logging!(trace, Type::Core, "Initializing core"); - if is_service_available().await.is_ok() { - Self::global().start_core_by_service().await?; - } else { - Self::global().start_core_by_sidecar().await?; - } + self.start_core().await?; logging!(trace, Type::Core, "Initied core"); #[cfg(target_os = "macos")] - log_err!(Tray::global().subscribe_traffic().await); + logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); Ok(()) } @@ -503,7 +539,10 @@ impl CoreManager { /// 启动核心 pub async fn start_core(&self) -> Result<()> { - if is_service_available().await.is_ok() { + if service::is_service_available().await.is_ok() { + if service::check_service_needs_reinstall().await { + service::reinstall_service().await?; + } self.start_core_by_service().await?; } else { self.start_core_by_sidecar().await?; diff --git a/clash-verge-rev/src-tauri/src/core/tray/mod.rs b/clash-verge-rev/src-tauri/src/core/tray/mod.rs index a5fff8fd5f..1648238918 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/mod.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/mod.rs @@ -8,7 +8,7 @@ use crate::{ feat, module::{lightweight::entry_lightweight_mode, mihomo::Rate}, resolve, - utils::{dirs, i18n::t, resolve::VERSION}, + utils::{dirs::find_target_icons, i18n::t, resolve::VERSION}, }; use anyhow::Result; @@ -20,10 +20,7 @@ use parking_lot::Mutex; use parking_lot::RwLock; #[cfg(target_os = "macos")] pub use speed_rate::{SpeedRate, Traffic}; -#[cfg(target_os = "macos")] -use std::collections::hash_map::DefaultHasher; -#[cfg(target_os = "macos")] -use std::hash::{Hash, Hasher}; +use std::fs; #[cfg(target_os = "macos")] use std::sync::Arc; use tauri::{ @@ -35,19 +32,124 @@ use tauri::{ use tokio::sync::broadcast; use super::handle; + +#[derive(Clone)] +struct TrayState {} + #[cfg(target_os = "macos")] pub struct Tray { pub speed_rate: Arc>>, shutdown_tx: Arc>>>, is_subscribed: Arc>, - pub icon_hash: Arc>>, - pub icon_cache: Arc>>>, pub rate_cache: Arc>>, } #[cfg(not(target_os = "macos"))] pub struct Tray {} +impl TrayState { + pub fn get_common_tray_icon() -> (bool, Vec) { + let verge = Config::verge().latest().clone(); + let is_common_tray_icon = verge.common_tray_icon.clone().unwrap_or(false); + if is_common_tray_icon { + if let Some(common_icon_path) = find_target_icons("common").unwrap() { + let icon_data = fs::read(common_icon_path).unwrap(); + return (true, icon_data); + } + } + #[cfg(target_os = "macos")] + { + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); + if tray_icon_colorful == "monochrome" { + ( + false, + include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(), + ) + } else { + ( + false, + include_bytes!("../../../icons/tray-icon.ico").to_vec(), + ) + } + } + + #[cfg(not(target_os = "macos"))] + { + ( + false, + include_bytes!("../../../icons/tray-icon.ico").to_vec(), + ) + } + } + + pub fn get_sysproxy_tray_icon() -> (bool, Vec) { + let verge = Config::verge().latest().clone(); + let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.clone().unwrap_or(false); + if is_sysproxy_tray_icon { + if let Some(sysproxy_icon_path) = find_target_icons("sysproxy").unwrap() { + let icon_data = fs::read(sysproxy_icon_path).unwrap(); + return (true, icon_data); + } + } + #[cfg(target_os = "macos")] + { + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); + if tray_icon_colorful == "monochrome" { + ( + false, + include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(), + ) + } else { + ( + false, + include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(), + ) + } + } + + #[cfg(not(target_os = "macos"))] + { + ( + false, + include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(), + ) + } + } + + pub fn get_tun_tray_icon() -> (bool, Vec) { + let verge = Config::verge().latest().clone(); + let is_tun_tray_icon = verge.tun_tray_icon.clone().unwrap_or(false); + if is_tun_tray_icon { + if let Some(tun_icon_path) = find_target_icons("tun").unwrap() { + let icon_data = fs::read(tun_icon_path).unwrap(); + return (true, icon_data); + } + } + #[cfg(target_os = "macos")] + { + let tray_icon_colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); + if tray_icon_colorful == "monochrome" { + ( + false, + include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(), + ) + } else { + ( + false, + include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(), + ) + } + } + #[cfg(not(target_os = "macos"))] + { + ( + false, + include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(), + ) + } + } +} + impl Tray { pub fn global() -> &'static Tray { static TRAY: OnceCell = OnceCell::new(); @@ -57,8 +159,6 @@ impl Tray { speed_rate: Arc::new(Mutex::new(None)), shutdown_tx: Arc::new(RwLock::new(None)), is_subscribed: Arc::new(RwLock::new(false)), - icon_hash: Arc::new(Mutex::new(None)), - icon_cache: Arc::new(Mutex::new(None)), rate_cache: Arc::new(Mutex::new(None)), }); @@ -159,107 +259,29 @@ impl Tray { } /// 更新托盘图标 - #[allow(unused_variables)] pub fn update_icon(&self, rate: Option) -> Result<()> { - let app_handle = handle::Handle::global().app_handle().unwrap(); let verge = Config::verge().latest().clone(); - let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); + let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false); - let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false); - let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false); - + let app_handle = handle::Handle::global().app_handle().unwrap(); let tray = app_handle.tray_by_id("main").unwrap(); - #[cfg(target_os = "macos")] - let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); - - let icon_bytes = if *system_proxy && !*tun_mode { - #[cfg(target_os = "macos")] - let mut icon = match tray_icon.as_str() { - "colorful" => include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(), - _ => include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(), - }; - - #[cfg(not(target_os = "macos"))] - let mut icon = include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(); - if *sysproxy_tray_icon { - let icon_dir_path = dirs::app_home_dir()?.join("icons"); - let png_path = icon_dir_path.join("sysproxy.png"); - let ico_path = icon_dir_path.join("sysproxy.ico"); - if ico_path.exists() { - icon = std::fs::read(ico_path).unwrap(); - } else if png_path.exists() { - icon = std::fs::read(png_path).unwrap(); - } - } - icon - } else if *tun_mode { - #[cfg(target_os = "macos")] - let mut icon = match tray_icon.as_str() { - "colorful" => include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(), - _ => include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(), - }; - - #[cfg(not(target_os = "macos"))] - let mut icon = include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(); - if *tun_tray_icon { - let icon_dir_path = dirs::app_home_dir()?.join("icons"); - let png_path = icon_dir_path.join("tun.png"); - let ico_path = icon_dir_path.join("tun.ico"); - if ico_path.exists() { - icon = std::fs::read(ico_path).unwrap(); - } else if png_path.exists() { - icon = std::fs::read(png_path).unwrap(); - } - } - icon - } else { - #[cfg(target_os = "macos")] - let mut icon = match tray_icon.as_str() { - "colorful" => include_bytes!("../../../icons/tray-icon.ico").to_vec(), - _ => include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(), - }; - - #[cfg(not(target_os = "macos"))] - let mut icon = include_bytes!("../../../icons/tray-icon.ico").to_vec(); - if *common_tray_icon { - let icon_dir_path = dirs::app_home_dir()?.join("icons"); - let png_path = icon_dir_path.join("common.png"); - let ico_path = icon_dir_path.join("common.ico"); - if ico_path.exists() { - icon = std::fs::read(ico_path).unwrap(); - } else if png_path.exists() { - icon = std::fs::read(png_path).unwrap(); - } - } - icon + let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { + (true, true) => TrayState::get_tun_tray_icon(), + (true, false) => TrayState::get_sysproxy_tray_icon(), + (false, true) => TrayState::get_tun_tray_icon(), + (false, false) => TrayState::get_common_tray_icon(), }; - #[cfg(target_os = "macos")] { let enable_tray_speed = verge.enable_tray_speed.unwrap_or(true); let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true); - let is_colorful = tray_icon == "colorful"; - - let icon_hash = { - let mut hasher = DefaultHasher::new(); - icon_bytes.clone().hash(&mut hasher); - hasher.finish() - }; - - let mut icon_hash_guard = self.icon_hash.lock(); - let mut icon_bytes_guard = self.icon_cache.lock(); - if *icon_hash_guard != Some(icon_hash) { - *icon_hash_guard = Some(icon_hash); - *icon_bytes_guard = Some(icon_bytes.clone()); - } + let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); + let is_colorful = colorful == "colorful"; if !enable_tray_speed { - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes( - &(*icon_bytes_guard).clone().unwrap(), - )?)); + let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); let _ = tray.set_icon_as_template(!is_colorful); return Ok(()); } @@ -280,16 +302,20 @@ impl Tray { *rate_guard = rate; let bytes = if enable_tray_icon { - Some(icon_bytes_guard.as_ref().unwrap()) + Some(icon_bytes) } else { None }; let rate = rate_guard.as_ref(); - let rate_bytes = SpeedRate::add_speed_text(bytes, rate).unwrap(); + let rate_bytes = SpeedRate::add_speed_text(is_custom_icon, bytes, rate).unwrap(); let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); - let _ = tray.set_icon_as_template(!is_colorful); + if !is_custom_icon { + let _ = tray.set_icon_as_template(!is_colorful); + } else { + let _ = tray.set_icon_as_template(false); + } } Ok(()) } diff --git a/clash-verge-rev/src-tauri/src/core/tray/speed_rate.rs b/clash-verge-rev/src-tauri/src/core/tray/speed_rate.rs index 089d8157da..6682124104 100644 --- a/clash-verge-rev/src-tauri/src/core/tray/speed_rate.rs +++ b/clash-verge-rev/src-tauri/src/core/tray/speed_rate.rs @@ -77,14 +77,15 @@ impl SpeedRate { // 分离图标加载和速率渲染 pub fn add_speed_text<'a>( - icon_bytes: Option<&'a Vec>, + is_custom_icon: bool, + icon_bytes: Option>, rate: Option<&'a Rate>, ) -> Result> { let rate = rate.unwrap_or(&Rate { up: 0, down: 0 }); let (mut icon_width, mut icon_height) = (0, 256); - let icon_image = if let Some(bytes) = icon_bytes { - let icon_image = image::load_from_memory(bytes)?; + let icon_image = if let Some(bytes) = icon_bytes.clone() { + let icon_image = image::load_from_memory(&bytes)?; icon_width = icon_image.width(); icon_height = icon_image.height(); icon_image @@ -94,23 +95,24 @@ impl SpeedRate { }; // 判断是否为彩色图标 - let is_colorful = if let Some(bytes) = icon_bytes { - !crate::utils::help::is_monochrome_image_from_bytes(bytes).unwrap_or(false) + let is_colorful = if let Some(bytes) = icon_bytes.clone() { + !crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false) } else { false }; - // 增加文本宽度和间距 - let total_width = if icon_bytes.is_some() { - if icon_width < 580 { - icon_width + 580 - } else { - icon_width - } - } else { - 580 + let total_width = match (is_custom_icon, icon_bytes.is_some()) { + (true, true) => 510, + (true, false) => 740, + (false, false) => 740, + (false, true) => icon_width + 740, }; + // println!( + // "icon_height: {}, icon_wight: {}, total_width: {}", + // icon_height, icon_width, total_width + // ); + // 创建新的透明画布 let mut combined_image = RgbaImage::new(total_width, icon_height); @@ -145,17 +147,30 @@ impl SpeedRate { let scale = ab_glyph::PxScale::from(font_size); // 使用更简洁的速率格式 - let up_text = format_bytes_speed(rate.up); - let down_text = format_bytes_speed(rate.down); + let up_text = format!("↑ {}", format_bytes_speed(rate.up)); + let down_text = format!("↓ {}", format_bytes_speed(rate.down)); + + // For test rate display + // let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024)); // 计算文本位置,确保垂直间距合适 // 修改文本位置为居右显示 - let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32; - let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32; - // 计算右对齐的文本位置 - let up_text_x = total_width - up_text_width; - let down_text_x = total_width - down_text_width; + // let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32; + // let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32; + // let up_text_x = total_width - up_text_width; + // let down_text_x = total_width - down_text_width; + + // 计算左对齐的文本位置 + let (up_text_x, down_text_x) = { + if is_custom_icon || icon_bytes.is_some() { + let text_left_offset = 30; + let left_begin = icon_width + text_left_offset; + (left_begin, left_begin) + } else { + (icon_width, icon_width) + } + }; // 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小 let text_height = font_size as i32; diff --git a/clash-verge-rev/src-tauri/src/lib.rs b/clash-verge-rev/src-tauri/src/lib.rs index 2e5d179ff6..228062489c 100644 --- a/clash-verge-rev/src-tauri/src/lib.rs +++ b/clash-verge-rev/src-tauri/src/lib.rs @@ -110,7 +110,6 @@ pub fn run() { .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) - .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) diff --git a/clash-verge-rev/src-tauri/src/utils/dirs.rs b/clash-verge-rev/src-tauri/src/utils/dirs.rs index 72b70a2c97..fc4f179ec4 100644 --- a/clash-verge-rev/src-tauri/src/utils/dirs.rs +++ b/clash-verge-rev/src-tauri/src/utils/dirs.rs @@ -77,9 +77,39 @@ pub fn app_profiles_dir() -> Result { Ok(app_home_dir()?.join("profiles")) } +/// icons dir +pub fn app_icons_dir() -> Result { + Ok(app_home_dir()?.join("icons")) +} + +pub fn find_target_icons(target: &str) -> Result> { + let icons_dir = app_icons_dir()?; + let mut matching_files = Vec::new(); + + for entry in fs::read_dir(icons_dir)? { + let entry = entry?; + let path = entry.path(); + + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + if file_name.starts_with(target) + && (file_name.ends_with(".ico") || file_name.ends_with(".png")) + { + matching_files.push(path); + } + } + } + + if matching_files.is_empty() { + Ok(None) + } else { + let first = path_to_str(matching_files.first().unwrap())?; + Ok(Some(first.to_string())) + } +} + /// logs dir pub fn app_logs_dir() -> Result { - Ok(app_home_dir()?.join("logs")) + Ok(app_home_dir()?.join("icons")) } pub fn clash_path() -> Result { diff --git a/clash-verge-rev/src-tauri/src/utils/help.rs b/clash-verge-rev/src-tauri/src/utils/help.rs index 2d4cef8e35..9d1b17c15c 100644 --- a/clash-verge-rev/src-tauri/src/utils/help.rs +++ b/clash-verge-rev/src-tauri/src/utils/help.rs @@ -167,15 +167,16 @@ macro_rules! t { /// ``` #[cfg(target_os = "macos")] pub fn format_bytes_speed(speed: u64) -> String { - if speed < 1024 { - format!("{}B/s", speed) - } else if speed < 1024 * 1024 { - format!("{:.1}KB/s", speed as f64 / 1024.0) - } else if speed < 1024 * 1024 * 1024 { - format!("{:.1}MB/s", speed as f64 / 1024.0 / 1024.0) - } else { - format!("{:.1}GB/s", speed as f64 / 1024.0 / 1024.0 / 1024.0) + const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; + let mut size = speed as f64; + let mut unit_index = 0; + + while size >= 1000.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; } + + format!("{:.1}{}/s", size, UNITS[unit_index]) } #[cfg(target_os = "macos")] diff --git a/clash-verge-rev/src-tauri/src/utils/logging.rs b/clash-verge-rev/src-tauri/src/utils/logging.rs index a2e00b4f83..4a46fb95ce 100644 --- a/clash-verge-rev/src-tauri/src/utils/logging.rs +++ b/clash-verge-rev/src-tauri/src/utils/logging.rs @@ -7,6 +7,7 @@ pub enum Type { Hotkey, Window, Config, + CMD, } impl fmt::Display for Type { @@ -17,6 +18,7 @@ impl fmt::Display for Type { Type::Hotkey => write!(f, "[Hotkey]"), Type::Window => write!(f, "[Window]"), Type::Config => write!(f, "[Config]"), + Type::CMD => write!(f, "[CMD]"), } } } diff --git a/clash-verge-rev/src-tauri/src/utils/resolve.rs b/clash-verge-rev/src-tauri/src/utils/resolve.rs index 87a6b7a9b8..9c7d9fbefd 100644 --- a/clash-verge-rev/src-tauri/src/utils/resolve.rs +++ b/clash-verge-rev/src-tauri/src/utils/resolve.rs @@ -18,7 +18,6 @@ use tauri::{App, Manager}; use tauri::Url; //#[cfg(not(target_os = "linux"))] // use window_shadows::set_shadow; -use tauri_plugin_notification::NotificationExt; pub static VERSION: OnceCell = OnceCell::new(); @@ -239,8 +238,6 @@ pub fn create_window() { pub async fn resolve_scheme(param: String) -> Result<()> { log::info!(target:"app", "received deep link: {}", param); - let app_handle = handle::Handle::global().app_handle().unwrap(); - let param_str = if param.starts_with("[") && param.len() > 4 { param .get(2..param.len() - 2) @@ -280,24 +277,9 @@ pub async fn resolve_scheme(param: String) -> Result<()> { let uid = item.uid.clone().unwrap(); let _ = wrap_err!(Config::profiles().data().append_item(item)); handle::Handle::notice_message("import_sub_url::ok", uid); - - app_handle - .notification() - .builder() - .title("Clash Verge") - .body("Import profile success") - .show() - .unwrap(); } Err(e) => { handle::Handle::notice_message("import_sub_url::error", e.to_string()); - app_handle - .notification() - .builder() - .title("Clash Verge") - .body(format!("Import profile failed: {e}")) - .show() - .unwrap(); } } } diff --git a/clash-verge-rev/src/components/home/enhanced-traffic-graph.tsx b/clash-verge-rev/src/components/home/enhanced-traffic-graph.tsx index 9c8d2ff1f6..b9aee07e37 100644 --- a/clash-verge-rev/src/components/home/enhanced-traffic-graph.tsx +++ b/clash-verge-rev/src/components/home/enhanced-traffic-graph.tsx @@ -39,69 +39,35 @@ export interface EnhancedTrafficGraphRef { // 时间范围类型 type TimeRange = 1 | 5 | 10; // 分钟 -// 创建一个明确的类型 +// 数据点类型 type DataPoint = ITrafficItem & { name: string; timestamp: number }; -// 控制帧率的工具函数 -const FPS_LIMIT = 1; // 限制为1fps,因为数据每秒才更新一次 -const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔,即1000ms - -// 全局存储流量数据历史记录 -declare global { - interface Window { - trafficHistoryData?: DataPoint[]; - trafficHistoryStyle?: "line" | "area"; - trafficHistoryTimeRange?: TimeRange; - } -} - -// 初始化全局存储 -if (typeof window !== "undefined" && !window.trafficHistoryData) { - window.trafficHistoryData = []; - window.trafficHistoryStyle = "area"; - window.trafficHistoryTimeRange = 10; -} - /** * 增强型流量图表组件 - * 基于 Recharts 实现,支持线图和面积图两种模式 */ export const EnhancedTrafficGraph = memo(forwardRef( (props, ref) => { const theme = useTheme(); const { t } = useTranslation(); - // 从全局变量恢复状态 - const [timeRange, setTimeRange] = useState( - window.trafficHistoryTimeRange || 10 - ); - const [chartStyle, setChartStyle] = useState<"line" | "area">( - window.trafficHistoryStyle || "area" - ); - - // 使用useRef存储数据,避免不必要的重渲染 - const dataBufferRef = useRef([]); - // 只为渲染目的的状态 + // 基础状态 + const [timeRange, setTimeRange] = useState(10); + const [chartStyle, setChartStyle] = useState<"line" | "area">("area"); const [displayData, setDisplayData] = useState([]); - // 帧率控制 - const lastUpdateTimeRef = useRef(0); - const pendingUpdateRef = useRef(false); - const rafIdRef = useRef(null); + // 数据缓冲区 + const dataBufferRef = useRef([]); // 根据时间范围计算保留的数据点数量 const getMaxPointsByTimeRange = useCallback( - (minutes: TimeRange): number => { - // 使用更低的采样率来减少点的数量,每2秒一个点而不是每秒一个点 - return minutes * 30; // 每分钟30个点(每2秒1个点) - }, - [], + (minutes: TimeRange): number => minutes * 30, + [] ); - // 最大数据点数量 - 基于选择的时间范围 + // 最大数据点数量 const MAX_BUFFER_SIZE = useMemo( () => getMaxPointsByTimeRange(10), - [getMaxPointsByTimeRange], + [getMaxPointsByTimeRange] ); // 颜色配置 @@ -113,150 +79,51 @@ export const EnhancedTrafficGraph = memo(forwardRef( tooltip: theme.palette.background.paper, text: theme.palette.text.primary, }), - [theme], + [theme] ); // 切换时间范围 const handleTimeRangeClick = useCallback(() => { setTimeRange((prevRange) => { // 在1、5、10分钟之间循环切换 - const newRange = prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; - window.trafficHistoryTimeRange = newRange; // 保存到全局 - return newRange; + return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; }); }, []); // 初始化数据缓冲区 useEffect(() => { - let initialBuffer: DataPoint[] = []; - - // 如果全局有保存的数据,优先使用 - if (window.trafficHistoryData && window.trafficHistoryData.length > 0) { - initialBuffer = [...window.trafficHistoryData]; - - // 确保数据长度符合要求 - if (initialBuffer.length > MAX_BUFFER_SIZE) { - initialBuffer = initialBuffer.slice(-MAX_BUFFER_SIZE); - } else if (initialBuffer.length < MAX_BUFFER_SIZE) { - // 如果历史数据不足,则在前面补充空数据 - const now = Date.now(); - const oldestTimestamp = initialBuffer.length > 0 - ? initialBuffer[0].timestamp - : now - 10 * 60 * 1000; - - const additionalPoints = MAX_BUFFER_SIZE - initialBuffer.length; - const timeInterval = initialBuffer.length > 0 - ? (initialBuffer[0].timestamp - (now - 10 * 60 * 1000)) / additionalPoints - : (10 * 60 * 1000) / MAX_BUFFER_SIZE; - - const emptyPrefix: DataPoint[] = Array.from( - { length: additionalPoints }, - (_, index) => { - const pointTime = oldestTimestamp - (additionalPoints - index) * timeInterval; - const date = new Date(pointTime); - - return { - up: 0, - down: 0, - timestamp: pointTime, - name: date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - } - ); - - initialBuffer = [...emptyPrefix, ...initialBuffer]; + // 创建初始空数据 + const now = Date.now(); + const tenMinutesAgo = now - 10 * 60 * 1000; + + const initialBuffer = Array.from( + { length: MAX_BUFFER_SIZE }, + (_, index) => { + const pointTime = + tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE); + const date = new Date(pointTime); + + return { + up: 0, + down: 0, + timestamp: pointTime, + name: date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }), + }; } - } else { - // 没有历史数据时,创建空的初始缓冲区 - const now = Date.now(); - const tenMinutesAgo = now - 10 * 60 * 1000; - - initialBuffer = Array.from( - { length: MAX_BUFFER_SIZE }, - (_, index) => { - const pointTime = - tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE); - const date = new Date(pointTime); - - return { - up: 0, - down: 0, - timestamp: pointTime, - name: date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }), - }; - } - ); - } + ); dataBufferRef.current = initialBuffer; - window.trafficHistoryData = initialBuffer; // 保存到全局 // 更新显示数据 const pointsToShow = getMaxPointsByTimeRange(timeRange); setDisplayData(initialBuffer.slice(-pointsToShow)); - - // 清理函数,取消任何未完成的动画帧 - return () => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } - }; }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]); - // 处理数据更新并控制帧率的函数 - const updateDisplayData = useCallback(() => { - if (pendingUpdateRef.current) { - pendingUpdateRef.current = false; - - // 根据当前时间范围计算需要显示的点数 - const pointsToShow = getMaxPointsByTimeRange(timeRange); - // 从缓冲区中获取最新的数据点 - const newDisplayData = dataBufferRef.current.slice(-pointsToShow); - setDisplayData(newDisplayData); - } - - rafIdRef.current = null; - }, [timeRange, getMaxPointsByTimeRange]); - - // 节流更新函数 - const throttledUpdateData = useCallback(() => { - pendingUpdateRef.current = true; - - const now = performance.now(); - const timeSinceLastUpdate = now - lastUpdateTimeRef.current; - - if (rafIdRef.current === null) { - if (timeSinceLastUpdate >= FRAME_MIN_TIME) { - // 如果距离上次更新已经超过最小帧时间,立即更新 - lastUpdateTimeRef.current = now; - rafIdRef.current = requestAnimationFrame(updateDisplayData); - } else { - // 否则,在适当的时间进行更新 - const timeToWait = FRAME_MIN_TIME - timeSinceLastUpdate; - setTimeout(() => { - lastUpdateTimeRef.current = performance.now(); - rafIdRef.current = requestAnimationFrame(updateDisplayData); - }, timeToWait); - } - } - }, [updateDisplayData]); - - // 监听时间范围变化,更新显示数据 - useEffect(() => { - throttledUpdateData(); - }, [timeRange, throttledUpdateData]); - // 添加数据点方法 const appendData = useCallback((data: ITrafficItem) => { // 安全处理数据 @@ -281,24 +148,24 @@ export const EnhancedTrafficGraph = memo(forwardRef( timestamp: timestamp, }; - // 更新ref,但保持原数组大小 + // 更新缓冲区,保持原数组大小 const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; dataBufferRef.current = newBuffer; - // 保存到全局变量 - window.trafficHistoryData = newBuffer; - - // 使用节流更新显示数据 - throttledUpdateData(); - }, [throttledUpdateData]); + // 更新显示数据 + const pointsToShow = getMaxPointsByTimeRange(timeRange); + setDisplayData(newBuffer.slice(-pointsToShow)); + }, [timeRange, getMaxPointsByTimeRange]); + + // 监听时间范围变化,更新显示数据 + useEffect(() => { + const pointsToShow = getMaxPointsByTimeRange(timeRange); + setDisplayData(dataBufferRef.current.slice(-pointsToShow)); + }, [timeRange, getMaxPointsByTimeRange]); // 切换图表样式 const toggleStyle = useCallback(() => { - setChartStyle((prev) => { - const newStyle = prev === "line" ? "area" : "line"; - window.trafficHistoryStyle = newStyle; // 保存到全局 - return newStyle; - }); + setChartStyle((prev) => prev === "line" ? "area" : "line"); }, []); // 暴露方法给父组件 @@ -308,13 +175,12 @@ export const EnhancedTrafficGraph = memo(forwardRef( appendData, toggleStyle, }), - [appendData, toggleStyle], + [appendData, toggleStyle] ); // 格式化工具提示内容 const formatTooltip = useCallback((value: number, name: string, props: any) => { const [num, unit] = parseTraffic(value); - // 使用props.dataKey判断是上传还是下载 return [`${num} ${unit}/s`, props?.dataKey === "up" ? t("Upload") : t("Download")]; }, [t]); @@ -327,7 +193,6 @@ export const EnhancedTrafficGraph = memo(forwardRef( // 格式化X轴标签 const formatXLabel = useCallback((value: string) => { if (!value) return ""; - // 只显示小时和分钟 const parts = value.split(":"); return `${parts[0]}:${parts[1]}`; }, []); @@ -352,7 +217,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( isAnimationActive: false, // 禁用动画以减少CPU使用 }), []); - // 曲线类型 - 使用线性曲线避免错位 + // 曲线类型 const curveType = "monotone"; return ( @@ -386,7 +251,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( tick={{ fontSize: 10, fill: colors.text }} tickLine={{ stroke: colors.grid }} axisLine={{ stroke: colors.grid }} - width={40} + width={43} domain={[0, "auto"]} /> ( tick={{ fontSize: 10, fill: colors.text }} tickLine={{ stroke: colors.grid }} axisLine={{ stroke: colors.grid }} - width={40} + width={43} domain={[0, "auto"]} padding={{ top: 10, bottom: 0 }} /> diff --git a/clash-verge-rev/src/components/setting/setting-clash.tsx b/clash-verge-rev/src/components/setting/setting-clash.tsx index 1993b88608..8de71ec1c1 100644 --- a/clash-verge-rev/src/components/setting/setting-clash.tsx +++ b/clash-verge-rev/src/components/setting/setting-clash.tsx @@ -5,7 +5,6 @@ import { SettingsRounded, ShuffleRounded, LanRounded, - DnsRounded, } from "@mui/icons-material"; import { DialogRef, Notice, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; @@ -49,7 +48,16 @@ const SettingClash = ({ onError }: Props) => { const { enable_random_port = false, verge_mixed_port } = verge ?? {}; // 独立跟踪DNS设置开关状态 - const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(false); + const [dnsSettingsEnabled, setDnsSettingsEnabled] = useState(() => { + // 尝试从localStorage获取之前保存的状态 + const savedState = localStorage.getItem("dns_settings_enabled"); + if (savedState !== null) { + return savedState === "true"; + } + // 如果没有保存的状态,则从verge配置中获取 + return verge?.enable_dns_settings ?? false; + }); + const { addListener } = useListen(); const webRef = useRef(null); @@ -59,12 +67,6 @@ const SettingClash = ({ onError }: Props) => { const networkRef = useRef(null); const dnsRef = useRef(null); - // 初始化时从verge配置中加载DNS设置开关状态 - useEffect(() => { - const dnsSettingsState = verge?.enable_dns_settings ?? false; - setDnsSettingsEnabled(dnsSettingsState); - }, [verge]); - const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { mutateClash((old) => ({ ...(old! || {}), ...patch }), false); @@ -84,15 +86,21 @@ const SettingClash = ({ onError }: Props) => { // 实现DNS设置开关处理函数 const handleDnsToggle = useLockFn(async (enable: boolean) => { try { + // 立即更新UI状态 setDnsSettingsEnabled(enable); + // 保存到localStorage,用于记住用户的选择 + localStorage.setItem("dns_settings_enabled", String(enable)); + // 更新verge配置 await patchVerge({ enable_dns_settings: enable }); await invoke("apply_dns_config", { apply: enable }); setTimeout(() => { mutateClash(); }, 500); // 延迟500ms确保后端完成处理 } catch (err: any) { - Notice.error(err.message || err.toString()); + // 如果出错,恢复原始状态 setDnsSettingsEnabled(!enable); + localStorage.setItem("dns_settings_enabled", String(!enable)); + Notice.error(err.message || err.toString()); await patchVerge({ enable_dns_settings: !enable }).catch(() => { // 忽略恢复状态时的错误 }); @@ -143,7 +151,6 @@ const SettingClash = ({ onError }: Props) => { /> } > - {/* 使用独立状态,不再依赖dns?.enable */} { const { data: autoLaunchEnabled } = useSWR( "getAutoLaunchStatus", getAutoLaunchStatus, + { revalidateOnFocus: false } ); // 当实际自启动状态与配置不同步时更新配置 diff --git a/clash-verge-rev/vite.config.mts b/clash-verge-rev/vite.config.mts index 9fb9b95c89..41efcda4d5 100644 --- a/clash-verge-rev/vite.config.mts +++ b/clash-verge-rev/vite.config.mts @@ -36,11 +36,96 @@ export default defineConfig({ entry: "monaco-yaml/yaml.worker", }, ], + globalAPI: false, }), ], build: { outDir: "../dist", emptyOutDir: true, + target: "esnext", + minify: "terser", + chunkSizeWarningLimit: 4000, + reportCompressedSize: false, + sourcemap: false, + cssCodeSplit: true, + cssMinify: true, + rollupOptions: { + treeshake: { + preset: "recommended", + moduleSideEffects: (id) => !/\.css$/.test(id), + tryCatchDeoptimization: false, + }, + output: { + compact: true, + experimentalMinChunkSize: 30000, + dynamicImportInCjs: true, + manualChunks(id) { + if (id.includes("node_modules")) { + // Monaco Editor should be a separate chunk + if (id.includes("monaco-editor")) return "monaco-editor"; + + // React-related libraries (react, react-dom, react-router-dom, etc.) + if ( + id.includes("react") || + id.includes("react-dom") || + id.includes("react-router-dom") || + id.includes("react-transition-group") || + id.includes("react-error-boundary") || + id.includes("react-hook-form") || + id.includes("react-markdown") || + id.includes("react-virtuoso") + ) { + return "react"; + } + + // Utilities chunk: group commonly used utility libraries + if ( + id.includes("axios") || + id.includes("lodash-es") || + id.includes("dayjs") || + id.includes("js-base64") || + id.includes("js-yaml") || + id.includes("cli-color") || + id.includes("nanoid") + ) { + return "utils"; + } + + // Tauri-related plugins: grouping together Tauri plugins + if ( + id.includes("@tauri-apps/api") || + id.includes("@tauri-apps/plugin-clipboard-manager") || + id.includes("@tauri-apps/plugin-dialog") || + id.includes("@tauri-apps/plugin-fs") || + id.includes("@tauri-apps/plugin-global-shortcut") || + id.includes("@tauri-apps/plugin-notification") || + id.includes("@tauri-apps/plugin-process") || + id.includes("@tauri-apps/plugin-shell") || + id.includes("@tauri-apps/plugin-updater") + ) { + return "tauri-plugins"; + } + + // Material UI libraries (grouped together) + if ( + id.includes("@mui/material") || + id.includes("@mui/icons-material") || + id.includes("@mui/lab") || + id.includes("@mui/x-data-grid") + ) { + return "mui"; + } + + // Small vendor packages + const pkg = id.match(/node_modules\/([^\/]+)/)?.[1]; + if (pkg && pkg.length < 8) return "small-vendors"; + + // Large vendor packages + return "large-vendor"; + } + }, + }, + }, }, resolve: { alias: { diff --git a/lede/target/linux/mediatek/patches-6.1/340-mtd-spinand-Add-support-for-the-Fidelix-FM35X1GA.patch b/lede/target/linux/mediatek/patches-6.1/340-mtd-spinand-Add-support-for-the-Fidelix-FM35X1GA.patch index ec66363dc9..5e219c3637 100644 --- a/lede/target/linux/mediatek/patches-6.1/340-mtd-spinand-Add-support-for-the-Fidelix-FM35X1GA.patch +++ b/lede/target/linux/mediatek/patches-6.1/340-mtd-spinand-Add-support-for-the-Fidelix-FM35X1GA.patch @@ -23,14 +23,14 @@ Signed-off-by: Davide Fioravanti obj-$(CONFIG_MTD_SPI_NAND) += spinand.o --- a/drivers/mtd/nand/spi/core.c +++ b/drivers/mtd/nand/spi/core.c -@@ -940,6 +940,7 @@ static const struct nand_ops spinand_ops - static const struct spinand_manufacturer *spinand_manufacturers[] = { +@@ -941,6 +941,7 @@ static const struct spinand_manufacturer &ato_spinand_manufacturer, &esmt_c8_spinand_manufacturer, -+ &fidelix_spinand_manufacturer, &etron_spinand_manufacturer, ++ &fidelix_spinand_manufacturer, &gigadevice_spinand_manufacturer, ¯onix_spinand_manufacturer, + µn_spinand_manufacturer, --- /dev/null +++ b/drivers/mtd/nand/spi/fidelix.c @@ -0,0 +1,76 @@ @@ -56,8 +56,8 @@ Signed-off-by: Davide Fioravanti + SPINAND_PROG_LOAD(true, 0, NULL, 0)); + +static SPINAND_OP_VARIANTS(update_cache_variants, -+ SPINAND_PROG_LOAD_X4(false, 0, NULL, 0), -+ SPINAND_PROG_LOAD(false, 0, NULL, 0)); ++ SPINAND_PROG_LOAD_X4(true, 0, NULL, 0), ++ SPINAND_PROG_LOAD(true, 0, NULL, 0)); + +static int fm35x1ga_ooblayout_ecc(struct mtd_info *mtd, int section, + struct mtd_oob_region *region) diff --git a/lede/target/linux/mediatek/patches-6.1/341-mtd-spinand-Add-support-fudanmicro-fm25s01b.patch b/lede/target/linux/mediatek/patches-6.1/341-mtd-spinand-Add-support-fudanmicro-fm25s01b.patch new file mode 100644 index 0000000000..456656456b --- /dev/null +++ b/lede/target/linux/mediatek/patches-6.1/341-mtd-spinand-Add-support-fudanmicro-fm25s01b.patch @@ -0,0 +1,133 @@ +--- a/drivers/mtd/nand/spi/Makefile ++++ b/drivers/mtd/nand/spi/Makefile +@@ -1,3 +1,3 @@ + # SPDX-License-Identifier: GPL-2.0 +-spinand-objs := core.o ato.o esmt.o etron.o fidelix.o gigadevice.o macronix.o micron.o paragon.o toshiba.o winbond.o xtx.o ++spinand-objs := core.o ato.o esmt.o etron.o fidelix.o fudanmicro.o gigadevice.o macronix.o micron.o paragon.o toshiba.o winbond.o xtx.o + obj-$(CONFIG_MTD_SPI_NAND) += spinand.o +--- a/drivers/mtd/nand/spi/core.c ++++ b/drivers/mtd/nand/spi/core.c +@@ -942,6 +942,7 @@ static const struct spinand_manufacturer + &esmt_c8_spinand_manufacturer, + &etron_spinand_manufacturer, + &fidelix_spinand_manufacturer, ++ &fudan_spinand_manufacturer, + &gigadevice_spinand_manufacturer, + ¯onix_spinand_manufacturer, + µn_spinand_manufacturer, +--- /dev/null ++++ b/drivers/mtd/nand/spi/fudanmicro.c +@@ -0,0 +1,103 @@ ++#include ++#include ++#include ++ ++#define SPINAND_MFR_FUDAN 0xA1 ++ ++#define FM25S01B_STATUS_ECC_MASK (7 << 4) ++#define STATUS_ECC_1_3_BITFLIPS (1 << 4) ++#define STATUS_ECC_4_6_BITFLIPS (3 << 4) ++#define STATUS_ECC_7_8_BITFLIPS (5 << 4) ++ ++static SPINAND_OP_VARIANTS(read_cache_variants, ++ SPINAND_PAGE_READ_FROM_CACHE_X4_OP(0, 1, NULL, 0), ++ SPINAND_PAGE_READ_FROM_CACHE_X2_OP(0, 1, NULL, 0), ++ SPINAND_PAGE_READ_FROM_CACHE_OP(true, 0, 1, NULL, 0), ++ SPINAND_PAGE_READ_FROM_CACHE_OP(false, 0, 1, NULL, 0)); ++ ++static SPINAND_OP_VARIANTS(write_cache_variants, ++ SPINAND_PROG_LOAD_X4(true, 0, NULL, 0), ++ SPINAND_PROG_LOAD(true, 0, NULL, 0)); ++ ++static SPINAND_OP_VARIANTS(update_cache_variants, ++ SPINAND_PROG_LOAD_X4(false, 0, NULL, 0), ++ SPINAND_PROG_LOAD(false, 0, NULL, 0)); ++ ++static int fm25s01b_ooblayout_ecc(struct mtd_info *mtd, int section, ++ struct mtd_oob_region *region) ++{ ++ if (section) ++ return -ERANGE; ++ ++ region->offset = 64; ++ region->length = 64; ++ ++ return 0; ++} ++ ++static int fm25s01b_ooblayout_free(struct mtd_info *mtd, int section, ++ struct mtd_oob_region *region) ++{ ++ if (section > 3) ++ return -ERANGE; ++ ++ region->offset = (16 * section) + 4; ++ region->length = 12; ++ ++ return 0; ++} ++ ++static const struct mtd_ooblayout_ops fm25s01b_ooblayout = { ++ .ecc = fm25s01b_ooblayout_ecc, ++ .free = fm25s01b_ooblayout_free, ++}; ++ ++static int fm25s01b_ecc_get_status(struct spinand_device *spinand, ++ u8 status) ++{ ++ switch (status & FM25S01B_STATUS_ECC_MASK) { ++ case STATUS_ECC_NO_BITFLIPS: ++ return 0; ++ ++ case STATUS_ECC_UNCOR_ERROR: ++ return -EBADMSG; ++ ++ case STATUS_ECC_1_3_BITFLIPS: ++ return 3; ++ ++ case STATUS_ECC_4_6_BITFLIPS: ++ return 6; ++ ++ case STATUS_ECC_7_8_BITFLIPS: ++ return 8; ++ ++ default: ++ break; ++ } ++ ++ return -EINVAL; ++} ++ ++static const struct spinand_info fudan_spinand_table[] = { ++ SPINAND_INFO("FM25S01B", ++ SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0xD4), ++ NAND_MEMORG(1, 2048, 128, 64, 1024, 20, 1, 1, 1), ++ NAND_ECCREQ(8, 512), ++ SPINAND_INFO_OP_VARIANTS(&read_cache_variants, ++ &write_cache_variants, ++ &update_cache_variants), ++ SPINAND_HAS_QE_BIT, ++ SPINAND_ECCINFO(&fm25s01b_ooblayout, ++ fm25s01b_ecc_get_status)), ++}; ++ ++static const struct spinand_manufacturer_ops fudan_spinand_manuf_ops = { ++}; ++ ++const struct spinand_manufacturer fudan_spinand_manufacturer = { ++ .id = SPINAND_MFR_FUDAN, ++ .name = "FUDAN Micron", ++ .chips = fudan_spinand_table, ++ .nchips = ARRAY_SIZE(fudan_spinand_table), ++ .ops = &fudan_spinand_manuf_ops, ++}; +--- a/include/linux/mtd/spinand.h ++++ b/include/linux/mtd/spinand.h +@@ -264,6 +264,7 @@ struct spinand_manufacturer { + extern const struct spinand_manufacturer esmt_c8_spinand_manufacturer; + extern const struct spinand_manufacturer etron_spinand_manufacturer; + extern const struct spinand_manufacturer fidelix_spinand_manufacturer; ++extern const struct spinand_manufacturer fudan_spinand_manufacturer; + extern const struct spinand_manufacturer gigadevice_spinand_manufacturer; + extern const struct spinand_manufacturer macronix_spinand_manufacturer; + extern const struct spinand_manufacturer micron_spinand_manufacturer; diff --git a/mihomo/listener/anytls/server.go b/mihomo/listener/anytls/server.go index 31a7c55ae9..aa8d946a6f 100644 --- a/mihomo/listener/anytls/server.go +++ b/mihomo/listener/anytls/server.go @@ -174,6 +174,12 @@ func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) { return } + // It seems that mihomo does not implement a connection error reporting mechanism, so we report success directly. + err = stream.HandshakeSuccess() + if err != nil { + return + } + h.NewConnection(ctx, stream, M.Metadata{ Source: M.SocksaddrFromNet(conn.RemoteAddr()), Destination: destination, diff --git a/mihomo/transport/anytls/padding/padding.go b/mihomo/transport/anytls/padding/padding.go index addd47c2fb..498feb05c0 100644 --- a/mihomo/transport/anytls/padding/padding.go +++ b/mihomo/transport/anytls/padding/padding.go @@ -15,10 +15,10 @@ import ( const CheckMark = -1 var DefaultPaddingScheme = []byte(`stop=8 -0=34-120 +0=30-30 1=100-400 -2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 -3=500-1000 +2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 +3=9-9,500-1000 4=500-1000 5=500-1000 6=500-1000 diff --git a/mihomo/transport/anytls/session/client.go b/mihomo/transport/anytls/session/client.go index 5e99f13562..50fd7b42ef 100644 --- a/mihomo/transport/anytls/session/client.go +++ b/mihomo/transport/anytls/session/client.go @@ -83,11 +83,7 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { } stream.dieHook = func() { - if session.IsClosed() { - if session.dieHook != nil { - session.dieHook() - } - } else { + if !session.IsClosed() { select { case <-c.die.Done(): // Now client has been closed @@ -154,10 +150,10 @@ func (c *Client) Close() error { c.sessionsLock.Lock() sessionToClose := make([]*Session, 0, len(c.sessions)) - for seq, session := range c.sessions { + for _, session := range c.sessions { sessionToClose = append(sessionToClose, session) - delete(c.sessions, seq) } + c.sessions = make(map[uint64]*Session) c.sessionsLock.Unlock() for _, session := range sessionToClose { diff --git a/mihomo/transport/anytls/session/frame.go b/mihomo/transport/anytls/session/frame.go index 49597c5526..8f6283213d 100644 --- a/mihomo/transport/anytls/session/frame.go +++ b/mihomo/transport/anytls/session/frame.go @@ -9,9 +9,14 @@ const ( // cmds cmdSYN = 1 // stream open cmdPSH = 2 // data push cmdFIN = 3 // stream close, a.k.a EOF mark - cmdSettings = 4 // Settings + cmdSettings = 4 // Settings (Client send to Server) cmdAlert = 5 // Alert cmdUpdatePaddingScheme = 6 // update padding scheme + // Since version 2 + cmdSYNACK = 7 // Server reports to the client that the stream has been opened + cmdHeartRequest = 8 // Keep alive command + cmdHeartResponse = 9 // Keep alive command + cmdServerSettings = 10 // Settings (Server send to client) ) const ( diff --git a/mihomo/transport/anytls/session/session.go b/mihomo/transport/anytls/session/session.go index 963533ead1..c80639ce3b 100644 --- a/mihomo/transport/anytls/session/session.go +++ b/mihomo/transport/anytls/session/session.go @@ -3,9 +3,11 @@ package session import ( "crypto/md5" "encoding/binary" + "fmt" "io" "net" "runtime/debug" + "strconv" "sync" "time" @@ -30,11 +32,16 @@ type Session struct { die chan struct{} dieHook func() + synDone func() + synDoneLock sync.Mutex + // pool seq uint64 idleSince time.Time padding *atomic.TypedValue[*padding.PaddingFactory] + peerVersion byte + // client isClient bool sendPadding bool @@ -76,7 +83,7 @@ func (s *Session) Run() { } settings := util.StringMap{ - "v": "1", + "v": "2", "client": "mihomo/" + constant.Version, "padding-md5": s.padding.Load().Md5, } @@ -105,15 +112,16 @@ func (s *Session) Close() error { close(s.die) once = true }) - if once { if s.dieHook != nil { s.dieHook() + s.dieHook = nil } s.streamLock.Lock() - for k := range s.streams { - s.streams[k].sessionClose() + for _, stream := range s.streams { + stream.Close() } + s.streams = make(map[uint32]*Stream) s.streamLock.Unlock() return s.conn.Close() } else { @@ -132,6 +140,17 @@ func (s *Session) OpenStream() (*Stream, error) { //logrus.Debugln("stream open", sid, s.streams) + if sid >= 2 && s.peerVersion >= 2 { + s.synDoneLock.Lock() + if s.synDone != nil { + s.synDone() + } + s.synDone = util.NewDeadlineWatcher(time.Second*3, func() { + s.Close() + }) + s.synDoneLock.Unlock() + } + if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { return nil, err } @@ -195,13 +214,37 @@ func (s *Session) recvLoop() error { if _, ok := s.streams[sid]; !ok { stream := newStream(sid, s) s.streams[sid] = stream - if s.onNewStream != nil { - go s.onNewStream(stream) - } else { - go s.Close() - } + go func() { + if s.onNewStream != nil { + s.onNewStream(stream) + } else { + stream.Close() + } + }() } s.streamLock.Unlock() + case cmdSYNACK: // should be client only + s.synDoneLock.Lock() + if s.synDone != nil { + s.synDone() + s.synDone = nil + } + s.synDoneLock.Unlock() + if hdr.Length() > 0 { + buffer := pool.Get(int(hdr.Length())) + if _, err := io.ReadFull(s.conn, buffer); err != nil { + pool.Put(buffer) + return err + } + // report error + s.streamLock.RLock() + stream, ok := s.streams[sid] + s.streamLock.RUnlock() + if ok { + stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer))) + } + pool.Put(buffer) + } case cmdFIN: s.streamLock.RLock() stream, ok := s.streams[sid] @@ -240,6 +283,20 @@ func (s *Session) recvLoop() error { return err } } + // check client's version + if v, err := strconv.Atoi(m["v"]); err == nil && v >= 2 { + s.peerVersion = byte(v) + // send cmdServerSettings + f := newFrame(cmdServerSettings, 0) + f.data = util.StringMap{ + "v": "2", + }.ToBytes() + _, err = s.writeFrame(f) + if err != nil { + pool.Put(buffer) + return err + } + } } pool.Put(buffer) } @@ -265,12 +322,35 @@ func (s *Session) recvLoop() error { } if s.isClient { if padding.UpdatePaddingScheme(rawScheme, s.padding) { - log.Infoln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) + log.Debugln("[Update padding succeed] %x\n", md5.Sum(rawScheme)) } else { log.Warnln("[Update padding failed] %x\n", md5.Sum(rawScheme)) } } } + case cmdHeartRequest: + if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil { + return err + } + case cmdHeartResponse: + // Active keepalive checking is not implemented yet + break + case cmdServerSettings: + if hdr.Length() > 0 { + buffer := pool.Get(int(hdr.Length())) + if _, err := io.ReadFull(s.conn, buffer); err != nil { + pool.Put(buffer) + return err + } + if s.isClient { + // check server's version + m := util.StringMapFromBytes(buffer) + if v, err := strconv.Atoi(m["v"]); err == nil { + s.peerVersion = byte(v) + } + } + pool.Put(buffer) + } default: // I don't know what command it is (can't have data) } @@ -280,8 +360,10 @@ func (s *Session) recvLoop() error { } } -// notify the session that a stream has closed func (s *Session) streamClosed(sid uint32) error { + if s.IsClosed() { + return io.ErrClosedPipe + } _, err := s.writeFrame(newFrame(cmdFIN, sid)) s.streamLock.Lock() delete(s.streams, sid) diff --git a/mihomo/transport/anytls/session/stream.go b/mihomo/transport/anytls/session/stream.go index 9f21ff04b3..1ec8fbc9be 100644 --- a/mihomo/transport/anytls/session/stream.go +++ b/mihomo/transport/anytls/session/stream.go @@ -22,6 +22,9 @@ type Stream struct { dieOnce sync.Once dieHook func() + dieErr error + + reportOnce sync.Once } // newStream initiates a Stream struct @@ -36,7 +39,11 @@ func newStream(id uint32, sess *Session) *Stream { // Read implements net.Conn func (s *Stream) Read(b []byte) (n int, err error) { - return s.pipeR.Read(b) + n, err = s.pipeR.Read(b) + if s.dieErr != nil { + err = s.dieErr + } + return } // Write implements net.Conn @@ -54,25 +61,28 @@ func (s *Stream) Write(b []byte) (n int, err error) { // Close implements net.Conn func (s *Stream) Close() error { - if s.sessionClose() { - // notify remote - return s.sess.streamClosed(s.id) - } else { - return io.ErrClosedPipe - } + return s.CloseWithError(io.ErrClosedPipe) } -// sessionClose close stream from session side, do not notify remote -func (s *Stream) sessionClose() (once bool) { +func (s *Stream) CloseWithError(err error) error { + // if err != io.ErrClosedPipe { + // logrus.Debugln(err) + // } + var once bool s.dieOnce.Do(func() { + s.dieErr = err s.pipeR.Close() once = true + }) + if once { if s.dieHook != nil { s.dieHook() s.dieHook = nil } - }) - return + return s.sess.streamClosed(s.id) + } else { + return s.dieErr + } } func (s *Stream) SetReadDeadline(t time.Time) error { @@ -108,3 +118,33 @@ func (s *Stream) RemoteAddr() net.Addr { } return nil } + +// HandshakeFailure should be called when Server fail to create outbound proxy +func (s *Stream) HandshakeFailure(err error) error { + var once bool + s.reportOnce.Do(func() { + once = true + }) + if once && err != nil && s.sess.peerVersion >= 2 { + f := newFrame(cmdSYNACK, s.id) + f.data = []byte(err.Error()) + if _, err := s.sess.writeFrame(f); err != nil { + return err + } + } + return nil +} + +// HandshakeSuccess should be called when Server success to create outbound proxy +func (s *Stream) HandshakeSuccess() error { + var once bool + s.reportOnce.Do(func() { + once = true + }) + if once && s.sess.peerVersion >= 2 { + if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil { + return err + } + } + return nil +} diff --git a/mihomo/transport/anytls/util/deadline.go b/mihomo/transport/anytls/util/deadline.go new file mode 100644 index 0000000000..8167bf9555 --- /dev/null +++ b/mihomo/transport/anytls/util/deadline.go @@ -0,0 +1,25 @@ +package util + +import ( + "sync" + "time" +) + +func NewDeadlineWatcher(ddl time.Duration, timeOut func()) (done func()) { + t := time.NewTimer(ddl) + closeCh := make(chan struct{}) + go func() { + defer t.Stop() + select { + case <-closeCh: + case <-t.C: + timeOut() + } + }() + var once sync.Once + return func() { + once.Do(func() { + close(closeCh) + }) + } +} diff --git a/openwrt-packages/alist/Makefile b/openwrt-packages/alist/Makefile index 38a12358e4..5f9db6233c 100644 --- a/openwrt-packages/alist/Makefile +++ b/openwrt-packages/alist/Makefile @@ -7,13 +7,13 @@ include $(TOPDIR)/rules.mk PKG_NAME:=alist -PKG_VERSION:=3.43.0 -PKG_WEB_VERSION:=3.43.0 +PKG_VERSION:=3.44.0 +PKG_WEB_VERSION:=3.44.0 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=803e3568d055053cbd345834237308708872cd6e154df1da7c00bc46803e4a8a +PKG_HASH:=8511e147e912933ba05c31c28f25c4245cc3529f854b0471ba947e33c09f297d PKG_LICENSE:=GPL-3.0 PKG_LICENSE_FILE:=LICENSE @@ -23,7 +23,7 @@ define Download/$(PKG_NAME)-web FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz URL_FILE:=dist.tar.gz URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/ - HASH:=22d1fcbd10af2257029c65add00a589201fabf9509ea57494a6d088b76a4e8a2 + HASH:=3709bec59bbc14f0f9f74193cebbb25a317c978fa3a5ae06a900eb341e1b5ae7 endef PKG_BUILD_DEPENDS:=golang/host diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index 101b815482..00f54184ed 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -11,7 +11,7 @@ end local fs = api.fs local sys = api.sys -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_geoview = api.is_finded("geoview") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 12adebebc8..7bdfb3ef3e 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -2,7 +2,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local datatypes = api.datatypes local fs = api.fs -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_geoview = api.is_finded("geoview") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua index b279f18feb..1df3decd65 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua @@ -4,7 +4,7 @@ local appname = "passwall" local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local ss_type = {} diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua index 8caa1ffb30..14adc205eb 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua @@ -25,7 +25,7 @@ end local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local ss_type = {} diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua index 87501e9cac..37e2d3bf32 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua @@ -1,7 +1,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local fs = api.fs -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_fw3 = api.is_finded("fw3") local has_fw4 = api.is_finded("fw4") diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua index 592823e3d3..a76184a76e 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua @@ -1,7 +1,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local has_xray = api.finded_com("xray") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") m = Map(appname) api.set_apply_on_parse(m) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua index 84a4ab2692..ffb269b0ee 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua @@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then luci.http.redirect(m.redirect) end -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local nodes_table = {} diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index 189be90f1e..f20b2cf0eb 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -2,7 +2,7 @@ local m, s = ... local api = require "luci.passwall.api" -local singbox_bin = api.finded_com("singbox") +local singbox_bin = api.finded_com("sing-box") local geoview_bin = api.is_finded("geoview") if not singbox_bin then diff --git a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua index 054848d93d..df52d1f455 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua @@ -2,7 +2,7 @@ local m, s = ... local api = require "luci.passwall.api" -local singbox_bin = api.finded_com("singbox") +local singbox_bin = api.finded_com("sing-box") if not singbox_bin then return diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/com.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/com.lua index 68407ee80f..8f658899d2 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/com.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/com.lua @@ -24,7 +24,7 @@ _M.hysteria = { } } -_M.singbox = { +_M["sing-box"] = { name = "Sing-Box", repo = "SagerNet/sing-box", get_url = gh_release_url, diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/server_app.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/server_app.lua index 50357056b6..9083ce2a61 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/server_app.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/server_app.lua @@ -142,7 +142,7 @@ local function start() bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) elseif type == "sing-box" then config = require(require_dir .. "util_sing-box").gen_config_server(user) - bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) + bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path) elseif type == "Xray" then config = require(require_dir .. "util_xray").gen_config_server(user) bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path) diff --git a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 5c916d97e8..56cee54f5c 100644 --- a/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/openwrt-passwall/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -7,7 +7,7 @@ local appname = "passwall" local fs = api.fs local split = api.split -local local_version = api.get_app_version("singbox") +local local_version = api.get_app_version("sing-box") local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0") local geosite_all_tag = {} diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/0_default_config b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/0_default_config index d665ed8edc..eea62fb28c 100644 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/0_default_config +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/0_default_config @@ -201,7 +201,7 @@ geosite:category-games' config shunt_rules 'AIGC' option remarks 'AIGC' - option domain_list 'geosite:category-ai-chat-!cn + option domain_list 'geosite:category-ai-!cn domain:apple-relay.apple.com' config shunt_rules 'Streaming' diff --git a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua index b1f8aca1f7..8dfb782c6d 100755 --- a/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/openwrt-passwall/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -24,7 +24,7 @@ uci:revert(appname) local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local allowInsecure_default = nil diff --git a/shadowsocks-rust/.github/workflows/build-nightly-release.yml b/shadowsocks-rust/.github/workflows/build-nightly-release.yml index 730a54aea8..895585b5f0 100644 --- a/shadowsocks-rust/.github/workflows/build-nightly-release.yml +++ b/shadowsocks-rust/.github/workflows/build-nightly-release.yml @@ -20,12 +20,12 @@ jobs: toolchain: stable - target: aarch64-unknown-linux-musl toolchain: stable - - target: mips-unknown-linux-gnu - toolchain: nightly - - target: mipsel-unknown-linux-gnu - toolchain: nightly - - target: mips64el-unknown-linux-gnuabi64 - toolchain: nightly + #- target: mips-unknown-linux-gnu + # toolchain: nightly + #- target: mipsel-unknown-linux-gnu + # toolchain: nightly + #- target: mips64el-unknown-linux-gnuabi64 + # toolchain: nightly steps: - name: Free Disk Space (Ubuntu) diff --git a/shadowsocks-rust/Cargo.lock b/shadowsocks-rust/Cargo.lock index caef0a3cf4..197acaf694 100644 --- a/shadowsocks-rust/Cargo.lock +++ b/shadowsocks-rust/Cargo.lock @@ -592,18 +592,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" dependencies = [ "anstream", "anstyle", @@ -1009,7 +1009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1980,7 +1980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2725,7 +2725,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3054,7 +3054,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3587,9 +3587,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.8" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" dependencies = [ "lock_api", ] @@ -3728,7 +3728,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4346,7 +4346,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml index 3e5df921d2..bec400a356 100644 --- a/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks-service/Cargo.toml @@ -129,7 +129,7 @@ once_cell = "1.17" thiserror = "2.0" arc-swap = "1.7" -spin = { version = "0.9" } +spin = { version = "0.10" } lru_time_cache = "0.11" bytes = "1.7" byte_string = "1.0" diff --git a/shadowsocks-rust/crates/shadowsocks/Cargo.toml b/shadowsocks-rust/crates/shadowsocks/Cargo.toml index b06816ccd3..caa9197985 100644 --- a/shadowsocks-rust/crates/shadowsocks/Cargo.toml +++ b/shadowsocks-rust/crates/shadowsocks/Cargo.toml @@ -58,7 +58,7 @@ byte_string = "1.0" base64 = "0.22" url = "2.5" once_cell = "1.17" -spin = { version = "0.9", features = ["std"] } +spin = { version = "0.10", features = ["std"] } pin-project = "1.1" bloomfilter = { version = "3.0.0", optional = true } thiserror = "2.0" diff --git a/sing-box/.github/workflows/build.yml b/sing-box/.github/workflows/build.yml index 68f2e804fe..349a570759 100644 --- a/sing-box/.github/workflows/build.yml +++ b/sing-box/.github/workflows/build.yml @@ -176,6 +176,9 @@ jobs: PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}" + PKG_VERSION="${{ needs.calculate_version.outputs.version }}" + PKG_VERSION="${PKG_VERSION//-/\~}" + echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}" - name: Package DEB if: matrix.debian != '' run: | @@ -183,7 +186,7 @@ jobs: sudo gem install fpm sudo apt-get install -y debsigs fpm -t deb \ - -v "${{ needs.calculate_version.outputs.version }}" \ + -v "$PKG_VERSION" \ -p "dist/${PKG_NAME}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box @@ -200,7 +203,7 @@ jobs: set -xeuo pipefail sudo gem install fpm fpm -t rpm \ - -v "${{ needs.calculate_version.outputs.version }}" \ + -v "$PKG_VERSION" \ -p "dist/${PKG_NAME}.rpm" \ --architecture ${{ matrix.rpm }} \ dist/sing-box=/usr/bin/sing-box @@ -219,7 +222,7 @@ jobs: sudo gem install fpm sudo apt-get install -y libarchive-tools fpm -t pacman \ - -v "${{ needs.calculate_version.outputs.version }}" \ + -v "$PKG_VERSION" \ -p "dist/${PKG_NAME}.pkg.tar.zst" \ --architecture ${{ matrix.pacman }} \ dist/sing-box=/usr/bin/sing-box diff --git a/sing-box/.github/workflows/linux.yml b/sing-box/.github/workflows/linux.yml index 79ef5ffecb..965929df90 100644 --- a/sing-box/.github/workflows/linux.yml +++ b/sing-box/.github/workflows/linux.yml @@ -109,6 +109,11 @@ jobs: if: contains(needs.calculate_version.outputs.version, '-') run: |- echo "NAME=sing-box-beta" >> "$GITHUB_ENV" + - name: Set version + run: |- + PKG_VERSION="${{ needs.calculate_version.outputs.version }}" + PKG_VERSION="${PKG_VERSION//-/\~}" + echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}" - name: Package DEB if: matrix.debian != '' run: | @@ -117,7 +122,7 @@ jobs: sudo apt-get install -y debsigs fpm -t deb \ --name "${NAME}" \ - -v "${{ needs.calculate_version.outputs.version }}" \ + -v "$PKG_VERSION" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \ --architecture ${{ matrix.debian }} \ dist/sing-box=/usr/bin/sing-box @@ -135,7 +140,7 @@ jobs: sudo gem install fpm fpm -t rpm \ --name "${NAME}" \ - -v "${{ needs.calculate_version.outputs.version }}" \ + -v "$PKG_VERSION" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \ --architecture ${{ matrix.rpm }} \ dist/sing-box=/usr/bin/sing-box diff --git a/sing-box/clients/android/version.properties b/sing-box/clients/android/version.properties index 404348b9b3..14c80ed8e0 100644 --- a/sing-box/clients/android/version.properties +++ b/sing-box/clients/android/version.properties @@ -1,3 +1,3 @@ -VERSION_CODE=488 -VERSION_NAME=1.11.5 +VERSION_CODE=497 +VERSION_NAME=1.11.6 GO_VERSION=go1.24.1 diff --git a/sing-box/dns/transport/quic/quic.go b/sing-box/dns/transport/quic/quic.go index fc5101ee4f..18f8b9febe 100644 --- a/sing-box/dns/transport/quic/quic.go +++ b/sing-box/dns/transport/quic/quic.go @@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C if err != nil { return nil, err } - defer stream.Close() - defer stream.CancelRead(0) err = transport.WriteMessage(stream, 0, message) if err != nil { + stream.Close() return nil, err } + stream.Close() return transport.ReadMessage(stream) } diff --git a/sing-box/docs/changelog.md b/sing-box/docs/changelog.md index f7aa6080d3..1fc835c3b1 100644 --- a/sing-box/docs/changelog.md +++ b/sing-box/docs/changelog.md @@ -2,10 +2,16 @@ icon: material/alert-decagram --- -#### 1.12.0-alpha.20 +#### 1.12.0-alpha.21 * Fixes and improvements +### 1.11.6 + +* Fixes and improvements + +_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._ + #### 1.12.0-alpha.19 * Update gVisor to 20250319.0 diff --git a/sing-box/docs/configuration/inbound/anytls.md b/sing-box/docs/configuration/inbound/anytls.md index 7eca943472..55790810f6 100644 --- a/sing-box/docs/configuration/inbound/anytls.md +++ b/sing-box/docs/configuration/inbound/anytls.md @@ -44,10 +44,10 @@ Default padding scheme: ``` stop=8 -0=34-120 +0=30-30 1=100-400 -2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 -3=500-1000 +2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 +3=9-9,500-1000 4=500-1000 5=500-1000 6=500-1000 diff --git a/sing-box/docs/configuration/inbound/anytls.zh.md b/sing-box/docs/configuration/inbound/anytls.zh.md index 5c119b724e..099da77724 100644 --- a/sing-box/docs/configuration/inbound/anytls.zh.md +++ b/sing-box/docs/configuration/inbound/anytls.zh.md @@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。 ``` stop=8 -0=34-120 +0=30-30 1=100-400 -2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500 -3=500-1000 +2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000 +3=9-9,500-1000 4=500-1000 5=500-1000 6=500-1000 diff --git a/sing-box/go.mod b/sing-box/go.mod index 9a970b461e..d5bd0b0cc2 100644 --- a/sing-box/go.mod +++ b/sing-box/go.mod @@ -3,7 +3,7 @@ module github.com/sagernet/sing-box go 1.23.1 require ( - github.com/anytls/sing-anytls v0.0.6 + github.com/anytls/sing-anytls v0.0.7 github.com/caddyserver/certmagic v0.21.7 github.com/cloudflare/circl v1.6.0 github.com/cretz/bine v0.2.0 diff --git a/sing-box/go.sum b/sing-box/go.sum index ed0ed5bd3f..1c20cdafad 100644 --- a/sing-box/go.sum +++ b/sing-box/go.sum @@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg= -github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= +github.com/anytls/sing-anytls v0.0.7 h1:0Q5dHNB2sqkFAWZCyK2vjQ/ckI5Iz3V/Frf3k7mBrGc= +github.com/anytls/sing-anytls v0.0.7/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= diff --git a/sing-box/protocol/tailscale/endpoint.go b/sing-box/protocol/tailscale/endpoint.go index f24f931f49..5a0731f755 100644 --- a/sing-box/protocol/tailscale/endpoint.go +++ b/sing-box/protocol/tailscale/endpoint.go @@ -2,8 +2,10 @@ package tailscale import ( "context" + "crypto/tls" "fmt" "net" + "net/http" "net/netip" "net/url" "os" @@ -212,6 +214,18 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { t.stack = ipStack localBackend := t.server.ExportLocalBackend() + localBackend.SetHTTPTestClient(&http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return t.server.Dialer.DialContext(ctx, network, M.ParseSocksaddr(address)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(t.ctx), + }, + }, + }) + perfs := &ipn.MaskedPrefs{ ExitNodeIPSet: true, AdvertiseRoutesSet: true, diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index 101b815482..00f54184ed 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -11,7 +11,7 @@ end local fs = api.fs local sys = api.sys -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_geoview = api.is_finded("geoview") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 12adebebc8..7bdfb3ef3e 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -2,7 +2,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local datatypes = api.datatypes local fs = api.fs -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_geoview = api.is_finded("geoview") local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist") diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua index b279f18feb..1df3decd65 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua @@ -4,7 +4,7 @@ local appname = "passwall" local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local ss_type = {} diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua index 8caa1ffb30..14adc205eb 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua @@ -25,7 +25,7 @@ end local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local ss_type = {} diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua index 87501e9cac..37e2d3bf32 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/other.lua @@ -1,7 +1,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local fs = api.fs -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_fw3 = api.is_finded("fw3") local has_fw4 = api.is_finded("fw4") diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua index 592823e3d3..a76184a76e 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/rule.lua @@ -1,7 +1,7 @@ local api = require "luci.passwall.api" local appname = "passwall" local has_xray = api.finded_com("xray") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") m = Map(appname) api.set_apply_on_parse(m) diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua index 84a4ab2692..ffb269b0ee 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/socks_config.lua @@ -9,7 +9,7 @@ if not arg[1] or not m:get(arg[1]) then luci.http.redirect(m.redirect) end -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local nodes_table = {} diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua index 189be90f1e..f20b2cf0eb 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/client/type/sing-box.lua @@ -2,7 +2,7 @@ local m, s = ... local api = require "luci.passwall.api" -local singbox_bin = api.finded_com("singbox") +local singbox_bin = api.finded_com("sing-box") local geoview_bin = api.is_finded("geoview") if not singbox_bin then diff --git a/small/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua b/small/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua index 054848d93d..df52d1f455 100644 --- a/small/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua +++ b/small/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua @@ -2,7 +2,7 @@ local m, s = ... local api = require "luci.passwall.api" -local singbox_bin = api.finded_com("singbox") +local singbox_bin = api.finded_com("sing-box") if not singbox_bin then return diff --git a/small/luci-app-passwall/luasrc/passwall/com.lua b/small/luci-app-passwall/luasrc/passwall/com.lua index 68407ee80f..8f658899d2 100644 --- a/small/luci-app-passwall/luasrc/passwall/com.lua +++ b/small/luci-app-passwall/luasrc/passwall/com.lua @@ -24,7 +24,7 @@ _M.hysteria = { } } -_M.singbox = { +_M["sing-box"] = { name = "Sing-Box", repo = "SagerNet/sing-box", get_url = gh_release_url, diff --git a/small/luci-app-passwall/luasrc/passwall/server_app.lua b/small/luci-app-passwall/luasrc/passwall/server_app.lua index 50357056b6..9083ce2a61 100644 --- a/small/luci-app-passwall/luasrc/passwall/server_app.lua +++ b/small/luci-app-passwall/luasrc/passwall/server_app.lua @@ -142,7 +142,7 @@ local function start() bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) elseif type == "sing-box" then config = require(require_dir .. "util_sing-box").gen_config_server(user) - bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) + bin = ln_run(api.get_app_path("sing-box"), "sing-box", "run -c " .. config_file, log_path) elseif type == "Xray" then config = require(require_dir .. "util_xray").gen_config_server(user) bin = ln_run(api.get_app_path("xray"), "xray", "run -c " .. config_file, log_path) diff --git a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 5c916d97e8..56cee54f5c 100644 --- a/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/small/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -7,7 +7,7 @@ local appname = "passwall" local fs = api.fs local split = api.split -local local_version = api.get_app_version("singbox") +local local_version = api.get_app_version("sing-box") local version_ge_1_11_0 = api.compare_versions(local_version:match("[^v]+"), ">=", "1.11.0") local geosite_all_tag = {} diff --git a/small/luci-app-passwall/root/usr/share/passwall/0_default_config b/small/luci-app-passwall/root/usr/share/passwall/0_default_config index d665ed8edc..eea62fb28c 100644 --- a/small/luci-app-passwall/root/usr/share/passwall/0_default_config +++ b/small/luci-app-passwall/root/usr/share/passwall/0_default_config @@ -201,7 +201,7 @@ geosite:category-games' config shunt_rules 'AIGC' option remarks 'AIGC' - option domain_list 'geosite:category-ai-chat-!cn + option domain_list 'geosite:category-ai-!cn domain:apple-relay.apple.com' config shunt_rules 'Streaming' diff --git a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua index b1f8aca1f7..8dfb782c6d 100755 --- a/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/small/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -24,7 +24,7 @@ uci:revert(appname) local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") local has_trojan_plus = api.is_finded("trojan-plus") -local has_singbox = api.finded_com("singbox") +local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") local allowInsecure_default = nil diff --git a/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua b/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua index 171f7d7928..d3095ec2b2 100644 --- a/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua +++ b/small/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua @@ -144,7 +144,7 @@ s = m:section(NamedSection, sid, "servers") s.anonymous = true s.addremove = false -o = s:option(DummyValue, "ssr_url", "SS/SSR/V2RAY/TROJAN URL") +o = s:option(DummyValue, "ssr_url", "SS/SSR/V2RAY/TROJAN/HYSTERIA2 URL") o.rawhtml = true o.template = "shadowsocksr/ssrurl" o.value = sid diff --git a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua index a5cacd9065..26944a4794 100755 --- a/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua +++ b/small/luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua @@ -10,7 +10,6 @@ require "luci.util" require "luci.sys" require "luci.jsonc" require "luci.model.ipkg" -local ucursor = require "luci.model.uci".cursor() -- these global functions are accessed all the time by the event handler -- so caching them is worth the effort @@ -76,11 +75,20 @@ local encrypt_methods_ss = { "camellia-256-cfb", "salsa20", "chacha20", - "chacha20-ietf" ]] + "chacha20-ietf" ]]-- } -- 分割字符串 local function split(full, sep) - full = full:gsub("%z", "") -- 这里不是很清楚 有时候结尾带个\0 + if full == nil or type(full) ~= "string" then + -- print("Debug: split() received nil or non-string value") + return {} + end + full = full:gsub("%z", ""):gsub("^%s+", ""):gsub("%s+$", "") -- 去除首尾空白字符和\0 + if full == "" then + -- print("Debug: split() received empty string after trimming") + return {} + end + sep = sep or "," -- 默认分隔符 local off, result = 1, {} while true do local nStart, nEnd = full:find(sep, off) @@ -155,13 +163,33 @@ local function checkTabValue(tab) end return revtab end +-- JSON完整性检查 +local function isCompleteJSON(str) + -- 检查JSON格式 + if type(str) ~= "string" or str:match("^%s*$") then + return false + end + -- 尝试解析JSON验证完整性 + local success, _ = pcall(jsonParse, str) + return success +end -- 处理数据 local function processData(szType, content) local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'} + -- 检查JSON的格式如不完整丢弃 + if not isCompleteJSON(content) then + return nil + end if szType == "hysteria2" then local url = URL.parse("http://" .. content) local params = url.query + -- 调试输出所有参数 + -- log("Hysteria2 原始参数:") + -- for k,v in pairs(params) do + -- log(k.."="..v) + -- end + result.alias = url.fragment and UrlDecode(url.fragment) or nil result.type = hy2_type result.server = url.host @@ -171,12 +199,12 @@ local function processData(szType, content) result.transport_protocol = params.protocol or "udp" end result.hy2_auth = url.user - result.uplink_capacity = params.upmbps - result.downlink_capacity = params.downmbps - if params.obfs and params.obfs-password then + result.uplink_capacity = params.upmbps or "5" + result.downlink_capacity = params.downmbps or "20" + if params.obfs then result.flag_obfs = "1" - result.transport_protocol = params.obfs - result.transport_protocol = params.obfs-password + result.obfs_type = params.obfs + result.salamander = params["obfs-password"] or params["obfs_password"] end if params.sni then result.tls = "1" @@ -210,8 +238,13 @@ local function processData(szType, content) result.alias = "[" .. group .. "] " end result.alias = result.alias .. base64Decode(params.remarks) - elseif szType == 'vmess' then - local info = jsonParse(content) + elseif szType == "vmess" then + -- 解析正常节点 + local success, info = pcall(jsonParse, content) + if not success or type(info) ~= "table" then + return nil + end + -- 处理有效数据 result.type = 'v2ray' result.v2ray_protocol = 'vmess' result.server = info.add @@ -312,12 +345,21 @@ local function processData(szType, content) idx_sp = content:find("#") alias = content:sub(idx_sp + 1, -1) end - local info = content:sub(1, idx_sp - 1) + local info = content:sub(1, idx_sp > 0 and idx_sp - 1 or #content) local hostInfo = split(base64Decode(info), "@") + if #hostInfo < 2 then + --log("SS节点格式错误,解码后内容:", base64Decode(info)) + return nil + end local host = split(hostInfo[2], ":") + if #host < 2 then + --log("SS节点主机格式错误:", hostInfo[2]) + return nil + end + -- 提取用户信息 local userinfo = base64Decode(hostInfo[1]) - local method = userinfo:sub(1, userinfo:find(":") - 1) - local password = userinfo:sub(userinfo:find(":") + 1, #userinfo) + local method, password = userinfo:match("^([^:]*):(.*)$") + -- 填充结果 result.alias = UrlDecode(alias) result.type = v2_ss result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil @@ -325,39 +367,47 @@ local function processData(szType, content) result.encrypt_method_ss = method result.password = password result.server = host[1] - if host[2]:find("/%?") then - local query = split(host[2], "/%?") + -- 处理端口和插件 + local port_part = host[2] + if port_part:find("/%?") then + local query = split(port_part, "/%?") result.server_port = query[1] - local params = {} - for _, v in pairs(split(query[2], '&')) do - local t = split(v, '=') - params[t[1]] = t[2] - end - if params.plugin then - local plugin_info = UrlDecode(params.plugin) - local idx_pn = plugin_info:find(";") - if idx_pn then - result.plugin = plugin_info:sub(1, idx_pn - 1) - result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info) - else - result.plugin = plugin_info - result.plugin_opts = "" + if query[2] then + local params = {} + for _, v in pairs(split(query[2], '&')) do + local t = split(v, '=') + if #t >= 2 then + params[t[1]] = t[2] + end end - -- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local - if result.plugin == "simple-obfs" then - result.plugin = "obfs-local" - end - -- 如果插件不為 none,確保 enable_plugin 為 1 - if result.plugin ~= "none" and result.plugin ~= "" then - result.enable_plugin = 1 + if params.plugin then + local plugin_info = UrlDecode(params.plugin) + local idx_pn = plugin_info:find(";") + if idx_pn then + result.plugin = plugin_info:sub(1, idx_pn - 1) + result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info) + else + result.plugin = plugin_info + result.plugin_opts = "" + end + -- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local + if result.plugin == "simple-obfs" then + result.plugin = "obfs-local" + end + -- 如果插件不为 none,确保 enable_plugin 为 1 + if result.plugin ~= "none" and result.plugin ~= "" then + result.enable_plugin = 1 + end end end else - result.server_port = host[2]:gsub("/","") + result.server_port = port_part:gsub("/","") end + -- 检查加密方法 if not checkTabValue(encrypt_methods_ss)[method] then - -- 1202 年了还不支持 SS AEAD 的屑机场 - result.server = nil + -- 1202 年了还不支持 SS AEAD 的屑机场 + -- log("不支持的SS加密方法:", method) + result.server = nil end elseif szType == "sip008" then result.type = v2_ss @@ -601,7 +651,7 @@ local function processData(szType, content) end -- wget local function wget(url) - local stdout = luci.sys.exec('wget-ssl -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"') + local stdout = luci.sys.exec('wget-ssl --timeout=20 --tries=3 -q --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" --no-check-certificate -O- "' .. url .. '"') return trim(stdout) end @@ -814,5 +864,3 @@ if subscribe_url and #subscribe_url > 0 then end end) end - - diff --git a/small/sing-box/Makefile b/small/sing-box/Makefile index dadaaf2b01..e7a8b8f5e2 100644 --- a/small/sing-box/Makefile +++ b/small/sing-box/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.11.5 +PKG_VERSION:=1.11.6 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=ac95287a65ae9297aa0b9f25934ead8468bf7ef3f9045e483659ddc400e758a1 +PKG_HASH:=01b697683c2433d93e3a15a362241e5e709c01a8e4cc97c889f6d2c5f9db275e PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE_FILES:=LICENSE diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index 8d9f5fd0e4..31de2822a0 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -21,13 +21,13 @@ define Download/geoip HASH:=83337c712b04d8c16351cf5a5394eae5cb9cfa257fb4773485945dce65dcea76 endef -GEOSITE_VER:=20250326132209 +GEOSITE_VER:=20250327125346 GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER) define Download/geosite URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/ URL_FILE:=dlc.dat FILE:=$(GEOSITE_FILE) - HASH:=1eeb1aa717971c6cabd5fa935a74e2a12c4f264b8641cec340309d85524895a7 + HASH:=a9114bff9cb6dcdf553b4cd21e56f589a65b7fb1836084b3c32b0acc6f2814a8 endef GEOSITE_IRAN_VER:=202503240038 diff --git a/socratex/.github/workflows/build.yml b/socratex/.github/workflows/build.yml index e8790a171b..fcbc24f452 100644 --- a/socratex/.github/workflows/build.yml +++ b/socratex/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 22 registry-url: https://registry.npmjs.org/ - run: git config --global user.name 'Leask Wong' - run: git config --global user.email 'i@leaskh.com' diff --git a/socratex/package.json b/socratex/package.json index 854617502a..21c9400277 100644 --- a/socratex/package.json +++ b/socratex/package.json @@ -1,7 +1,7 @@ { "name": "socratex", "description": "A Secure Web Proxy. Which is fast, secure, and easy to use.", - "version": "2.0.18", + "version": "2.0.20", "private": false, "homepage": "https://github.com/Leask/socratex", "main": "index.mjs", diff --git a/v2raya/service/core/v2ray/process.go b/v2raya/service/core/v2ray/process.go index 9f0dafca19..2406d96a5d 100644 --- a/v2raya/service/core/v2ray/process.go +++ b/v2raya/service/core/v2ray/process.go @@ -151,7 +151,7 @@ func NewProcess(tmpl *Template, } return nil, fmt.Errorf("unexpected exiting: check the log for more information") } - if time.Since(startTime) > 15*time.Second { + if time.Since(startTime) > 30*time.Second { return nil, fmt.Errorf("timeout: check the log for more information") } time.Sleep(100 * time.Millisecond) diff --git a/v2rayn/v2rayN/ServiceLib/Global.cs b/v2rayn/v2rayN/ServiceLib/Global.cs index f962efe1a3..da70326bdd 100644 --- a/v2rayn/v2rayN/ServiceLib/Global.cs +++ b/v2rayn/v2rayN/ServiceLib/Global.cs @@ -72,6 +72,7 @@ namespace ServiceLib public const string LocalAppData = "V2RAYN_LOCAL_APPLICATION_DATA_V2"; public const string V2RayLocalAsset = "V2RAY_LOCATION_ASSET"; public const string XrayLocalAsset = "XRAY_LOCATION_ASSET"; + public const string XrayLocalCert = "XRAY_LOCATION_CERT"; public const int SpeedTestPageSize = 1000; public const string LinuxBash = "/bin/bash"; diff --git a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs index 0c05db6436..8e225cd40a 100644 --- a/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs +++ b/v2rayn/v2rayN/ServiceLib/Handler/CoreHandler.cs @@ -24,6 +24,7 @@ namespace ServiceLib.Handler Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process); //Copy the bin folder to the storage location (for init) if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1") diff --git a/v2rayng/V2rayNG/app/build.gradle.kts b/v2rayng/V2rayNG/app/build.gradle.kts index bdeface771..54964506df 100644 --- a/v2rayng/V2rayNG/app/build.gradle.kts +++ b/v2rayng/V2rayNG/app/build.gradle.kts @@ -148,7 +148,7 @@ dependencies { // UI Libraries implementation(libs.material) - implementation(libs.toastcompat) + implementation(libs.toasty) implementation(libs.editorkit) implementation(libs.flexbox) diff --git a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml index eae2d9469f..b74faef368 100644 --- a/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml +++ b/v2rayng/V2rayNG/app/src/main/AndroidManifest.xml @@ -35,7 +35,6 @@ - + android:process=":RunSoLibV2RayDaemon" + tools:targetApi="24"> diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt index 266be605b2..3064535f51 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/extension/_Ext.kt @@ -8,7 +8,7 @@ import android.os.Build import android.os.Bundle import android.widget.Toast import com.v2ray.ang.AngApplication -import me.drakeet.support.toast.ToastCompat +import es.dmoral.toasty.Toasty import org.json.JSONObject import java.io.Serializable import java.net.URI @@ -23,7 +23,7 @@ val Context.v2RayApplication: AngApplication? * @param message The resource ID of the message to show. */ fun Context.toast(message: Int) { - ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() + Toasty.normal(this, message).show() } /** @@ -32,9 +32,46 @@ fun Context.toast(message: Int) { * @param message The text of the message to show. */ fun Context.toast(message: CharSequence) { - ToastCompat.makeText(this, message, Toast.LENGTH_SHORT).show() + Toasty.normal(this, message).show() } +/** + * Shows a toast message with the given resource ID. + * + * @param message The resource ID of the message to show. + */ +fun Context.toastSuccess(message: Int) { + Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given text. + * + * @param message The text of the message to show. + */ +fun Context.toastSuccess(message: CharSequence) { + Toasty.success(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given resource ID. + * + * @param message The resource ID of the message to show. + */ +fun Context.toastError(message: Int) { + Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() +} + +/** + * Shows a toast message with the given text. + * + * @param message The text of the message to show. + */ +fun Context.toastError(message: CharSequence) { + Toasty.error(this, message, Toast.LENGTH_SHORT, true).show() +} + + /** * Puts a key-value pair into the JSONObject. * diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt index 1f7201ab4b..18362be04c 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/AboutActivity.kt @@ -14,6 +14,8 @@ import com.v2ray.ang.BuildConfig import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityAboutBinding import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.SpeedtestManager import com.v2ray.ang.util.Utils import com.v2ray.ang.util.ZipUtil @@ -50,9 +52,9 @@ class AboutActivity : BaseActivity() { binding.layoutBackup.setOnClickListener { val ret = backupConfiguration(extDir.absolutePath) if (ret.first) { - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } @@ -72,7 +74,7 @@ class AboutActivity : BaseActivity() { ) ) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } @@ -126,7 +128,7 @@ class AboutActivity : BaseActivity() { } } - fun backupConfiguration(outputZipFilePos: String): Pair { + private fun backupConfiguration(outputZipFilePos: String): Pair { val dateFormated = SimpleDateFormat( "yyyy-MM-dd-HH-mm-ss", Locale.getDefault() @@ -147,7 +149,7 @@ class AboutActivity : BaseActivity() { } } - fun restoreConfiguration(zipFile: File): Boolean { + private fun restoreConfiguration(zipFile: File): Boolean { val backupDir = this.cacheDir.absolutePath + "/${System.currentTimeMillis()}" if (!ZipUtil.unzipToFolder(zipFile, backupDir)) { @@ -185,13 +187,13 @@ class AboutActivity : BaseActivity() { } } if (restoreConfiguration(targetFile)) { - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } catch (e: Exception) { Log.e(AppConfig.ANG_PACKAGE, "Error during file restore: ${e.message}", e) - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt index 3ee5b399b3..90d8f6ddda 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/LogcatActivity.kt @@ -1,5 +1,6 @@ package com.v2ray.ang.ui +import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -11,6 +12,7 @@ import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -21,7 +23,7 @@ import java.io.IOException class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { private val binding by lazy { ActivityLogcatBinding.inflate(layoutInflater) } - var logsetsAll: MutableList = mutableListOf() + private var logsetsAll: MutableList = mutableListOf() var logsets: MutableList = mutableListOf() private val adapter by lazy { LogcatRecyclerAdapter(this) } @@ -62,7 +64,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { launch(Dispatchers.Main) { logsetsAll = allText.toMutableList() logsets = allText.toMutableList() - adapter.notifyDataSetChanged() + refreshData() binding.refreshLayout.isRefreshing = false } } @@ -84,7 +86,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { launch(Dispatchers.Main) { logsetsAll.clear() logsets.clear() - adapter.notifyDataSetChanged() + refreshData() } } } catch (e: IOException) { @@ -118,7 +120,7 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { R.id.copy_all -> { Utils.setClipboard(this, logsets.joinToString("\n")) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) true } @@ -138,11 +140,16 @@ class LogcatActivity : BaseActivity(), SwipeRefreshLayout.OnRefreshListener { logsetsAll.filter { it.contains(key) }.toMutableList() } - adapter?.notifyDataSetChanged() + refreshData() return true } override fun onRefresh() { getLogcat() } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + adapter.notifyDataSetChanged() + } } \ No newline at end of file diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index 8356c5abbb..e8946e7b7e 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -1,6 +1,7 @@ package com.v2ray.ang.ui import android.Manifest +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.content.res.ColorStateList @@ -31,6 +32,7 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityMainBinding import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.MigrateManager import com.v2ray.ang.handler.MmkvManager @@ -204,6 +206,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList }) } + @SuppressLint("NotifyDataSetChanged") private fun setupViewModel() { mainViewModel.updateListAction.observe(this) { index -> if (index >= 0) { @@ -269,7 +272,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList binding.tabGroup.isVisible = true } - fun startV2Ray() { + private fun startV2Ray() { if (MmkvManager.getSelectServer().isNullOrEmpty()) { toast(R.string.title_file_chooser) return @@ -277,7 +280,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList V2RayServiceManager.startVService(this) } - fun restartV2Ray() { + private fun restartV2Ray() { if (mainViewModel.isRunning.value == true) { V2RayServiceManager.stopVService(this) } @@ -503,13 +506,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } countSub > 0 -> initGroupTab() - else -> toast(R.string.toast_failure) + else -> toastError(R.string.toast_failure) } binding.pbWaiting.hide() } } catch (e: Exception) { withContext(Dispatchers.Main) { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) binding.pbWaiting.hide() } e.printStackTrace() @@ -613,7 +616,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList toast(getString(R.string.title_update_config_count, count)) mainViewModel.reloadServerList() } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } binding.pbWaiting.hide() } @@ -629,7 +632,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList if (ret > 0) toast(getString(R.string.title_export_config_count, ret)) else - toast(R.string.toast_failure) + toastError(R.string.toast_failure) binding.pbWaiting.hide() } } @@ -759,9 +762,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList // } // if (mainViewModel.appendCustomConfigServer(server)) { // mainViewModel.reloadServerList() -// toast(R.string.toast_success) +// toastSuccess(R.string.toast_success) // } else { -// toast(R.string.toast_failure) +// toastError(R.string.toast_failure) // } // //adapter.notifyItemInserted(mainViewModel.serverList.lastIndex) // } catch (e: Exception) { diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt index 4c92456e40..8729355f61 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -19,6 +19,8 @@ import com.v2ray.ang.databinding.ItemRecyclerMainBinding import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.ItemTouchHelperAdapter @@ -43,6 +45,10 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter, skip: Int) { AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i -> try { @@ -166,28 +192,46 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0 } - appsList.sortedWith(Comparator { p1, p2 -> + appsList.sortedWith { p1, p2 -> when { p1.isSelected > p2.isSelected -> -1 p1.isSelected == p2.isSelected -> 0 else -> 1 } - }) + } } else { val collator = Collator.getInstance() appsList.sortedWith(compareBy(collator) { it.appName }) @@ -112,8 +114,10 @@ class PerAppProxyActivity : BaseActivity() { return super.onCreateOptionsMenu(menu) } + + @SuppressLint("NotifyDataSetChanged") override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { - R.id.select_all -> adapter?.let { + R.id.select_all -> adapter?.let { it -> val pkgNames = it.apps.map { it.packageName } if (it.blacklist.containsAll(pkgNames)) { it.apps.forEach { @@ -162,7 +166,7 @@ class PerAppProxyActivity : BaseActivity() { launch(Dispatchers.Main) { Log.d(ANG_PACKAGE, content) selectProxyApp(content, true) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) binding.pbWaiting.hide() } } @@ -172,7 +176,7 @@ class PerAppProxyActivity : BaseActivity() { val content = Utils.getClipboard(applicationContext) if (TextUtils.isEmpty(content)) return selectProxyApp(content, false) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } private fun exportProxyApp() { @@ -182,9 +186,10 @@ class PerAppProxyActivity : BaseActivity() { lst = lst + System.getProperty("line.separator") + it } Utils.setClipboard(applicationContext, lst) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } + @SuppressLint("NotifyDataSetChanged") private fun selectProxyApp(content: String, force: Boolean): Boolean { try { val proxyApps = if (TextUtils.isEmpty(content)) { @@ -197,7 +202,7 @@ class PerAppProxyActivity : BaseActivity() { adapter?.blacklist?.clear() if (binding.switchBypassApps.isChecked) { - adapter?.let { + adapter?.let { it -> it.apps.forEach block@{ val packageName = it.packageName Log.d(ANG_PACKAGE, packageName) @@ -210,7 +215,7 @@ class PerAppProxyActivity : BaseActivity() { it.notifyDataSetChanged() } } else { - adapter?.let { + adapter?.let { it -> it.apps.forEach block@{ val packageName = it.packageName Log.d(ANG_PACKAGE, packageName) @@ -259,7 +264,12 @@ class PerAppProxyActivity : BaseActivity() { adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist) binding.recyclerView.adapter = adapter - adapter?.notifyDataSetChanged() + refreshData() return true } + + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + adapter?.notifyDataSetChanged() + } } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt index 561db0e966..bb0ee572e1 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingEditActivity.kt @@ -9,6 +9,7 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityRoutingEditBinding import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers @@ -78,7 +79,7 @@ class RoutingEditActivity : BaseActivity() { } SettingsManager.saveRoutingRuleset(position, rulesetItem) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) finish() return true } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt index 609f0e6c02..fbda7e7187 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/RoutingSettingActivity.kt @@ -1,6 +1,7 @@ package com.v2ray.ang.ui import android.Manifest +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.Menu @@ -17,6 +18,8 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityRoutingSettingBinding import com.v2ray.ang.dto.RulesetItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.helper.SimpleItemTouchHelperCallback @@ -104,7 +107,7 @@ class RoutingSettingActivity : BaseActivity() { SettingsManager.resetRoutingRulesetsFromPresets(this@RoutingSettingActivity, i) launch(Dispatchers.Main) { refreshData() - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } } } catch (e: Exception) { @@ -125,7 +128,7 @@ class RoutingSettingActivity : BaseActivity() { Utils.getClipboard(this) } catch (e: Exception) { e.printStackTrace() - toast(R.string.toast_failure) + toastError(R.string.toast_failure) return@setPositiveButton } lifecycleScope.launch(Dispatchers.IO) { @@ -133,9 +136,9 @@ class RoutingSettingActivity : BaseActivity() { withContext(Dispatchers.Main) { if (result) { refreshData() - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } } @@ -149,10 +152,10 @@ class RoutingSettingActivity : BaseActivity() { private fun export2Clipboard() { val rulesetList = MmkvManager.decodeRoutingRulesets() if (rulesetList.isNullOrEmpty()) { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } else { Utils.setClipboard(this, JsonUtil.toJson(rulesetList)) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } } @@ -170,9 +173,9 @@ class RoutingSettingActivity : BaseActivity() { withContext(Dispatchers.Main) { if (result) { refreshData() - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } } @@ -184,6 +187,7 @@ class RoutingSettingActivity : BaseActivity() { return true } + @SuppressLint("NotifyDataSetChanged") fun refreshData() { rulesets.clear() rulesets.addAll(MmkvManager.decodeRoutingRulesets() ?: mutableListOf()) diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt index 0d9a7d2231..9a972f937f 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ScScannerActivity.kt @@ -6,6 +6,8 @@ import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import com.v2ray.ang.R import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.AngConfigManager class ScScannerActivity : BaseActivity() { @@ -27,7 +29,7 @@ class ScScannerActivity : BaseActivity() { importQRcode() } - fun importQRcode(): Boolean { + private fun importQRcode(): Boolean { requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) return true } @@ -38,9 +40,9 @@ class ScScannerActivity : BaseActivity() { val (count, countSub) = AngConfigManager.importBatchConfig(scanResult, "", false) if (count + countSub > 0) { - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } startActivity(Intent(this, MainActivity::class.java)) diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt index 5600852c41..9e88a7bdb2 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerActivity.kt @@ -28,6 +28,7 @@ import com.v2ray.ang.dto.NetworkType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.isNotNullEmpty import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.JsonUtil import com.v2ray.ang.util.Utils @@ -354,11 +355,11 @@ class ServerActivity : BaseActivity() { container_alpn?.visibility = View.VISIBLE et_sni?.text = Utils.getEditable(config.sni) - config.fingerPrint?.let { + config.fingerPrint?.let { it -> val utlsIndex = Utils.arrayFind(uTlsItems, it) utlsIndex.let { sp_stream_fingerprint?.setSelection(if (it >= 0) it else 0) } } - config.alpn?.let { + config.alpn?.let { it -> val alpnIndex = Utils.arrayFind(alpns, it) alpnIndex.let { sp_stream_alpn?.setSelection(if (it >= 0) it else 0) } } @@ -482,7 +483,7 @@ class ServerActivity : BaseActivity() { } Log.d(ANG_PACKAGE, JsonUtil.toJsonPretty(config) ?: "") MmkvManager.encodeServerConfig(editGuid, config) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) finish() return true } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt index ecc36088b1..903ec41027 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/ServerCustomConfigActivity.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.text.TextUtils import android.view.Menu import android.view.MenuItem -import android.widget.Toast import androidx.appcompat.app.AlertDialog import com.blacksquircle.ui.editorkit.utils.EditorTheme import com.blacksquircle.ui.language.json.JsonLanguage @@ -13,10 +12,10 @@ import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.fmt.CustomFmt import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils -import me.drakeet.support.toast.ToastCompat class ServerCustomConfigActivity : BaseActivity() { private val binding by lazy { ActivityServerCustomConfigBinding.inflate(layoutInflater) } @@ -91,7 +90,7 @@ class ServerCustomConfigActivity : BaseActivity() { MmkvManager.encodeServerConfig(editGuid, config) MmkvManager.encodeServerRaw(editGuid, binding.editor.text.toString()) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) finish() return true } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt index 45f92c2df0..3f5f92808b 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubEditActivity.kt @@ -10,6 +10,7 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubEditBinding import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils import kotlinx.coroutines.Dispatchers @@ -18,8 +19,8 @@ import kotlinx.coroutines.launch class SubEditActivity : BaseActivity() { private val binding by lazy { ActivitySubEditBinding.inflate(layoutInflater) } - var del_config: MenuItem? = null - var save_config: MenuItem? = null + private var del_config: MenuItem? = null + private var save_config: MenuItem? = null private val editSubId by lazy { intent.getStringExtra("subId").orEmpty() } @@ -37,7 +38,7 @@ class SubEditActivity : BaseActivity() { } /** - * bingding seleced server config + * binding selected server config */ private fun bindingServer(subItem: SubscriptionItem): Boolean { binding.etRemarks.text = Utils.getEditable(subItem.remarks) @@ -94,7 +95,7 @@ class SubEditActivity : BaseActivity() { } MmkvManager.encodeSubscription(editSubId, subItem) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) finish() return true } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt index 2957c59bb8..d15f06e769 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt @@ -1,5 +1,6 @@ package com.v2ray.ang.ui +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle import android.view.Menu @@ -11,6 +12,8 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivitySubSettingBinding import com.v2ray.ang.dto.SubscriptionItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.helper.SimpleItemTouchHelperCallback @@ -64,9 +67,9 @@ class SubSettingActivity : BaseActivity() { delay(500L) launch(Dispatchers.Main) { if (count > 0) { - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) } else { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } binding.pbWaiting.hide() } @@ -79,6 +82,7 @@ class SubSettingActivity : BaseActivity() { } + @SuppressLint("NotifyDataSetChanged") fun refreshData() { subscriptions = MmkvManager.decodeSubscriptions() adapter.notifyDataSetChanged() diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt index 16eac3a45e..7c5f7ee5d6 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/TaskerActivity.kt @@ -28,7 +28,7 @@ class TaskerActivity : BaseActivity() { lstData.add("Default") lstGuid.add(AppConfig.TASKER_DEFAULT_GUID) - MmkvManager.decodeServerList()?.forEach { key -> + MmkvManager.decodeServerList().forEach { key -> MmkvManager.decodeServerConfig(key)?.let { config -> lstData.add(config.remarks) lstGuid.add(key) diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt index 8d5c2b4212..264e19dc2d 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UrlSchemeActivity.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.lifecycleScope import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityLogcatBinding import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError import com.v2ray.ang.handler.AngConfigManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -44,7 +45,7 @@ class UrlSchemeActivity : BaseActivity() { } else -> { - toast(R.string.toast_failure) + toastError(R.string.toast_failure) } } } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index b1274f6841..387ea3e829 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -26,6 +26,8 @@ import com.v2ray.ang.databinding.ItemRecyclerUserAssetBinding import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.extension.toTrafficString import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager import com.v2ray.ang.util.HttpUtil @@ -91,7 +93,7 @@ class UserAssetActivity : BaseActivity() { override fun onResume() { super.onResume() - binding.recyclerView.adapter?.notifyDataSetChanged() + refreshData() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -117,7 +119,7 @@ class UserAssetActivity : BaseActivity() { requestStoragePermissionLauncher.launch(permission) } - val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + private val chooseFile = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val uri = result.data?.data if (result.resultCode == RESULT_OK && uri != null) { val assetId = Utils.getUuid() @@ -135,7 +137,7 @@ class UserAssetActivity : BaseActivity() { copyFile(uri) } }.onFailure { - toast(R.string.toast_asset_copy_failed) + toastError(R.string.toast_asset_copy_failed) MmkvManager.removeAssetUrl(assetId) } } @@ -146,8 +148,8 @@ class UserAssetActivity : BaseActivity() { contentResolver.openInputStream(uri).use { inputStream -> targetFile.outputStream().use { fileOut -> inputStream?.copyTo(fileOut) - toast(R.string.toast_success) - binding.recyclerView.adapter?.notifyDataSetChanged() + toastSuccess(R.string.toast_success) + refreshData() } } return targetFile.path @@ -215,7 +217,7 @@ class UserAssetActivity : BaseActivity() { withContext(Dispatchers.Main) { if (resultCount > 0) { toast(getString(R.string.title_update_config_count, resultCount)) - binding.recyclerView.adapter?.notifyDataSetChanged() + refreshData() } else { toast(getString(R.string.toast_failure)) } @@ -269,11 +271,16 @@ class UserAssetActivity : BaseActivity() { lifecycleScope.launch(Dispatchers.Default) { SettingsManager.initAssets(this@UserAssetActivity, assets) withContext(Dispatchers.Main) { - binding.recyclerView.adapter?.notifyDataSetChanged() + refreshData() } } } + @SuppressLint("NotifyDataSetChanged") + fun refreshData() { + binding.recyclerView.adapter?.notifyDataSetChanged() + } + inner class UserAssetAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder { return UserAssetViewHolder( diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt index 0f95dd7ad6..2fad2c0fb1 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetUrlActivity.kt @@ -9,6 +9,7 @@ import com.v2ray.ang.R import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding import com.v2ray.ang.dto.AssetUrlItem import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.util.Utils import java.io.File @@ -21,10 +22,10 @@ class UserAssetUrlActivity : BaseActivity() { private val binding by lazy { ActivityUserAssetUrlBinding.inflate(layoutInflater) } - var del_config: MenuItem? = null - var save_config: MenuItem? = null + private var del_config: MenuItem? = null + private var save_config: MenuItem? = null - val extDir by lazy { File(Utils.userAssetPath(this)) } + private val extDir by lazy { File(Utils.userAssetPath(this)) } private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() } override fun onCreate(savedInstanceState: Bundle?) { @@ -101,7 +102,7 @@ class UserAssetUrlActivity : BaseActivity() { } MmkvManager.encodeAsset(assetId, assetItem) - toast(R.string.toast_success) + toastSuccess(R.string.toast_success) finish() return true } diff --git a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt index 84648a5e37..bafbe0d302 100644 --- a/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt +++ b/v2rayng/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -19,6 +19,8 @@ import com.v2ray.ang.dto.ProfileItem import com.v2ray.ang.dto.ServersCache import com.v2ray.ang.extension.serializable import com.v2ray.ang.extension.toast +import com.v2ray.ang.extension.toastError +import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.AngConfigManager import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsManager @@ -419,12 +421,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { } AppConfig.MSG_STATE_START_SUCCESS -> { - getApplication().toast(R.string.toast_services_success) + getApplication().toastSuccess(R.string.toast_services_success) isRunning.value = true } AppConfig.MSG_STATE_START_FAILURE -> { - getApplication().toast(R.string.toast_services_failure) + getApplication().toastError(R.string.toast_services_failure) isRunning.value = false } diff --git a/v2rayng/V2rayNG/app/src/main/res/values-fa/strings.xml b/v2rayng/V2rayNG/app/src/main/res/values-fa/strings.xml index d3bc008c80..b01f49a8ef 100644 --- a/v2rayng/V2rayNG/app/src/main/res/values-fa/strings.xml +++ b/v2rayng/V2rayNG/app/src/main/res/values-fa/strings.xml @@ -214,8 +214,8 @@ پروکسی HTTP را به VPN اضافه کنید پروکسی HTTP مستقیماً از (مرورگر/برخی برنامه‌های پشتیبانی‌شده)، بدون استفاده از دستگاه NIC مجازی (Android 10+) استفاده می‌شود. - Enable double column display - The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect. + فعال کردن نمایش دو ستون + لیست نمایه در دو ستون نمایش داده می شود و امکان نمایش محتوای بیشتری را بر روی صفحه نمایش می دهد. برای اجرا باید برنامه را مجددا راه اندازی کنید. بازخورد @@ -327,10 +327,10 @@ QRcode - Export to clipboard - Export full configuration to clipboard - Edit - Delete + صادر کردن به کلیپ بورد + صادر کردن پیکربندی کامل به کلیپ بورد + ویرایش کردن + حذف کردن diff --git a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml index 4cc097f1d2..ec3cf3ff06 100644 --- a/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/v2rayng/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -215,8 +215,8 @@ Дополнительный HTTP-прокси HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+) - Enable double column display - The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect. + Отображение в два столбца + Список профилей выводится в виде двух столбцов, что позволяет показать больше информации на экране. Требуется перезапуск приложения. Обратная связь @@ -328,11 +328,11 @@ - QRcode - Export to clipboard - Export full configuration to clipboard - Edit - Delete + QR-код + Экспорт в буфер обмена + Экспорт всей конфигурации в буфер обмена + Изменить + Удалить diff --git a/v2rayng/V2rayNG/gradle/libs.versions.toml b/v2rayng/V2rayNG/gradle/libs.versions.toml index 0e53dae05a..b0d1ea91a9 100644 --- a/v2rayng/V2rayNG/gradle/libs.versions.toml +++ b/v2rayng/V2rayNG/gradle/libs.versions.toml @@ -17,7 +17,7 @@ quickieFoss = "1.14.0" kotlinx-coroutines-android = "1.10.1" kotlinx-coroutines-core = "1.10.1" swiperefreshlayout = "1.1.0" -toastcompat = "1.1.0" +toasty = "1.5.2" editorkit = "2.9.0" core = "3.5.3" workRuntimeKtx = "2.10.0" @@ -44,7 +44,7 @@ gson = { module = "com.google.code.gson:gson", version.ref = "gson" } quickie-foss = { module = "com.github.T8RIN.QuickieExtended:quickie-foss", version.ref = "quickieFoss" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "kotlinx-coroutines-android" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "kotlinx-coroutines-core" } -toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastcompat" } +toasty = { module = "com.github.GrenderG:Toasty", version.ref = "toasty" } editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" } language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" } language-json = { module = "com.blacksquircle.ui:language-json", version.ref = "editorkit" }