diff --git a/.github/workflows/alpha.yml b/.github/workflows/alpha.yml index 8f9836989..a7221a367 100644 --- a/.github/workflows/alpha.yml +++ b/.github/workflows/alpha.yml @@ -185,18 +185,18 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) + if [ -f "Changelog.md" ]; then + UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' Changelog.md) if [ -n "$UPDATE_LOGS" ]; then echo "Found update logs" echo "UPDATE_LOGS<> $GITHUB_ENV echo "$UPDATE_LOGS" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else - echo "No update sections found in UPDATELOG.md" + echo "No update sections found in Changelog.md" fi else - echo "UPDATELOG.md file not found" + echo "Changelog.md file not found" fi shell: bash diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 65178f415..0f978e8a3 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -35,20 +35,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in UPDATELOG.md" - fi - else - echo "UPDATELOG.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - uses: pnpm/action-setup@v4 @@ -158,7 +145,10 @@ jobs: uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -214,6 +204,13 @@ jobs: - name: Release ${{ env.TAG_CHANNEL }} Version run: pnpm release-version autobuild-latest + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build for Windows-macOS-Linux uses: tauri-apps/tauri-action@v0 env: @@ -257,7 +254,10 @@ jobs: uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.91.0" + targets: ${{ matrix.target }} - name: Add Rust Target run: rustup target add ${{ matrix.target }} @@ -347,6 +347,13 @@ jobs: gcc-arm-linux-gnueabihf \ g++-arm-linux-gnueabihf + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri Build for Linux run: | export PKG_CONFIG_ALLOW_CROSS=1 @@ -446,6 +453,13 @@ jobs: Remove-Item .\src-tauri\tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json + - name: Add Rust Target + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build for Windows id: build uses: tauri-apps/tauri-action@v0 @@ -509,20 +523,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in UPDATELOG.md" - fi - else - echo "UPDATELOG.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Install Node diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 336c5e422..e5ae26c32 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -74,11 +74,7 @@ jobs: - name: Install Rust Stable if: github.event.inputs[matrix.input] == 'true' - uses: dtolnay/rust-toolchain@stable - - - name: Add Rust Target - if: github.event.inputs[matrix.input] == 'true' - run: rustup target add ${{ matrix.target }} + uses: dtolnay/rust-toolchain@1.91.0 - name: Rust Cache if: github.event.inputs[matrix.input] == 'true' @@ -118,6 +114,14 @@ jobs: if: github.event.inputs[matrix.input] == 'true' run: pnpm release-version ${{ env.TAG_NAME }} + - name: Add Rust Target + if: github.event.inputs[matrix.input] == 'true' + run: | + # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed + rustup target add ${{ matrix.target }} --toolchain 1.91.0 || rustup target add ${{ matrix.target }} + rustup target list --installed + echo "Rust target ${{ matrix.target }} installed." + - name: Tauri build if: github.event.inputs[matrix.input] == 'true' uses: tauri-apps/tauri-action@v0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0385c16c9..153a5f8c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,20 +73,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in UPDATELOG.md" - fi - else - echo "UPDATELOG.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Set Env @@ -552,20 +539,7 @@ jobs: - name: Fetch UPDATE logs id: fetch_update_logs - run: | - if [ -f "UPDATELOG.md" ]; then - UPDATE_LOGS=$(awk '/^## v/{if(flag) exit; flag=1} flag' UPDATELOG.md) - if [ -n "$UPDATE_LOGS" ]; then - echo "Found update logs" - echo "UPDATE_LOGS<> $GITHUB_ENV - echo "$UPDATE_LOGS" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - else - echo "No update sections found in UPDATELOG.md" - fi - else - echo "UPDATELOG.md file not found" - fi + run: bash ./scripts/extract_update_logs.sh shell: bash - name: Install Node diff --git a/.prettierignore b/.prettierignore index 0a3b69148..bc817cd59 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,5 @@ # README.md -# UPDATELOG.md +# Changelog.md # CONTRIBUTING.md pnpm-lock.yaml diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 000000000..91ec1ed5e --- /dev/null +++ b/Changelog.md @@ -0,0 +1,248 @@ +## v2.4.4 + +### 🐞 修复问题 + +- Linux 无法切换 TUN 堆栈 + +
+ ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.16** +- 支持连接页面各个项目的排序 +- 实现可选的自动备份 + +
+ +
+ 🚀 优化改进 + +- 替换前端信息编辑组件,提供更好性能 +- 优化后端内存和性能表现 +- 防止退出时可能的禁用 TUN 失败 +- i18n 支持 +- 优化备份设置布局 +- 优化流量图性能表现,实现动态 FPS 和窗口失焦自动暂停 + +
+ +## v2.4.3 + +**发行代号:澜** +代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。 + +特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献 + +### 🐞 修复问题 + +- 优化服务模式重装逻辑,避免不必要的重复检查 +- 修复轻量模式退出无响应的问题 +- 修复托盘轻量模式支持退出/进入 +- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 +- macOS Tun/系统代理 模式下图标大小不统一 +- 托盘节点切换不再显示隐藏组 +- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 +- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 +- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 +- 修复 Webdav 恢复备份不重启 +- 修复 Linux 开机后无法正常代理需要手动设置 +- 修复增加订阅或导入订阅文件时订阅页面无更新 +- 修复系统代理守卫功能不工作 +- 修复 KDE + Wayland 下多屏显示 UI 异常 +- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 +- 修复静默启动不加载完整 WebView 的问题 +- 修复 Linux WebKit 网络进程的崩溃 +- 修复无法导入订阅 +- 修复实际导入成功但显示导入失败的问题 +- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 +- 修复删除订阅时未能实际删除相关文件 +- 修复 macOS 连接界面显示异常 +- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 +- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 +- 修复自动更新使版本回退的问题 +- 修复首页自定义卡片在切换轻量模式时失效 +- 修复悬浮跳转导航失效 +- 修复小键盘热键映射错误 +- 修复前端无法及时刷新操作状态 +- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 +- 修复 Linux 系统主题切换不生效 +- 修复 `允许自动更新` 字段使手动订阅刷新失效 +- 修复轻量模式托盘状态不同步 +- 修复一键导入订阅导致应用卡死崩溃的问题 + +
+ ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.15** +- 支持前端修改日志(最大文件大小、最大保留数量) +- 新增链式代理图形化设置功能 +- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏) +- 监听关机事件,自动关闭系统代理 +- 主界面“当前节点”卡片新增“延迟测试”按钮 +- 新增批量选择配置文件功能 +- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置 +- 新增本地备份功能 +- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭) +- 允许独立控制订阅自动更新 +- 托盘 `更多` 中新增 `关闭所有连接` 按钮 +- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏) +- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志` +
+ +
+ 🚀 优化改进 + +- 重构并简化服务模式启动检测流程,消除重复检测 +- 重构并简化窗口创建流程 +- 重构日志系统,单个日志默认最大 10 MB +- 优化前端资源占用 +- 改进 macos 下系统代理设置的方法 +- 优化 TUN 模式可用性的判断 +- 移除流媒体检测的系统级提示(使用软件内通知) +- 优化后端 i18n 资源占用 +- 改进 Linux 托盘支持并添加 `--no-tray` 选项 +- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式 +- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL +- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端 +- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核 +- 修改内核默认日志级别为 Info +- 支持通过桌面快捷方式重新打开应用 +- 支持订阅界面输入链接后回车导入 +- 选择按延迟排序时每次延迟测试自动刷新节点顺序 +- 配置重载失败时自动重启核心 +- 启用 TUN 前等待服务就绪 +- 卸载 TUN 时会先关闭 +- 优化应用启动页 +- 优化首页当前节点对MATCH规则的支持 +- 允许在 `界面设置` 修改 `悬浮跳转导航延迟` +- 添加热键绑定错误的提示信息 +- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 +- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 +- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` +- 允许设置 Mihomo 端口范围 1000(含) - 65536(含) + +
+ +## v2.4.2 + +### ✨ 新增功能 + +- 增加托盘节点选择 + +### 🚀 性能优化 + +- 优化前端首页加载速度 +- 优化前端未使用 i18n 文件缓存 +- 优化后端内存占用 +- 优化后端启动速度 + +### 🐞 修复问题 + +- 修复首页节点切换失效的问题 +- 修复和优化服务检查流程 +- 修复2.4.1引入的订阅地址重定向报错问题 +- 修复 rpm/deb 包名称问题 +- 修复托盘轻量模式状态检测异常 +- 修复通过 scheme 导入订阅崩溃 +- 修复单例检测实效 +- 修复启动阶段可能导致的无法连接内核 +- 修复导入订阅无法 Auth Basic + +### 👙 界面样式 + +- 简化和改进代理设置样式 + +## v2.4.1 + +### 🏆 重大改进 + +- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.13** + +### 🚀 性能优化 + +- 优化热键响应速度,提升快捷键操作体验 +- 改进服务管理响应性,减少系统服务操作等待时间 +- 提升文件和配置处理性能 +- 优化任务管理和日志记录效率 +- 优化异步内存管理,减少内存占用并提升多任务处理效率 +- 优化启动阶段初始化性能 + +### 🐞 修复问题 + +- 修复应用在某些操作中可能出现的响应延迟问题 +- 修复任务管理中的潜在并发问题 +- 修复通过托盘重启应用无法恢复 +- 修复订阅在某些情况下无法导入 +- 修复无法新建订阅时使用远程链接 +- 修复卸载服务后的 tun 开关状态问题 +- 修复页面快速切换订阅时导致崩溃 +- 修复丢失工作目录时无法恢复环境 +- 修复从轻量模式恢复导致崩溃 + +### 👙 界面样式 + +- 统一代理设置样式 + +### 🗑️ 移除内容 + +- 移除启动阶段自动清理过期订阅 + +## v2.4.0 + +**发行代号:融** +代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。 + +### 🏆 重大改进 + +- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性 +- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示 +- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度 + +### ✨ 新增功能 + +- **Mihomo(Meta) 内核升级至 v1.19.12** +- 新增版本信息复制按钮 +- 增强型流量监控,支持更详细的数据分析 +- 新增流量图表多种显示模式 +- 新增强制刷新配置和节点缓存功能 +- 首页流量统计支持查看刻度线详情 + +### 🚀 性能优化 + +- 全面提升数据传输和处理效率 +- 优化内存使用,减少系统资源消耗 +- 改进流量图表渲染性能 +- 优化配置和节点刷新策略,从5秒延长到60秒 +- 改进数据缓存机制,减少重复请求 +- 优化异步程序性能 + +### 🐞 修复问题 + +- 修复系统代理状态检测和显示不一致问题 +- 修复系统主题窗口颜色不一致问题 +- 修复特殊字符 URL 处理问题 +- 修复配置修改后缓存不同步问题 +- 修复 Windows 安装器自启设置问题 +- 修复 macOS 下 Dock 图标恢复窗口问题 +- 修复 linux 下 KDE/Plasma 异常标题栏按钮 +- 修复架构升级后节点测速功能异常 +- 修复架构升级后流量统计功能异常 +- 修复架构升级后日志功能异常 +- 修复外部控制器跨域配置保存问题 +- 修复首页端口显示不一致问题 +- 修复首页流量统计刻度线显示问题 +- 修复日志页面按钮功能混淆问题 +- 修复日志等级设置保存问题 +- 修复日志等级异常过滤 +- 修复清理日志天数功能异常 +- 修复偶发性启动卡死问题 +- 修复首页虚拟网卡开关在管理模式下的状态问题 + +### 🔧 技术改进 + +- 统一使用新的内核通信方式 +- 新增外部控制器配置界面 +- 改进跨平台兼容性支持 diff --git a/UPDATELOG.md b/docs/Changelog.history.md similarity index 85% rename from UPDATELOG.md rename to docs/Changelog.history.md index c26123b1d..115ab2d91 100644 --- a/UPDATELOG.md +++ b/docs/Changelog.history.md @@ -1,240 +1,3 @@ -# v2.4.4 - -### 🐞 修复问题 - -
- ✨ 新增功能 -
- -
- 🚀 优化改进 - -- 替换前端信息编辑组件,提供更好性能 - -
- -## v2.4.3 - -**发行代号:澜** -代号释义:澜象征平稳与融合,本次版本聚焦稳定性、兼容性、性能与体验优化,全面提升整体可靠性。 - -特别感谢 @Slinetrac, @oomeow, @Lythrilla, @Dragon1573 的出色贡献 - -### 🐞 修复问题 - -- 优化服务模式重装逻辑,避免不必要的重复检查 -- 修复轻量模式退出无响应的问题 -- 修复托盘轻量模式支持退出/进入 -- 修复静默启动和自动进入轻量模式时,托盘状态刷新不再依赖窗口创建流程 -- macOS Tun/系统代理 模式下图标大小不统一 -- 托盘节点切换不再显示隐藏组 -- 修复前端 IP 检测无法使用 ipapi, ipsb 提供商 -- 修复MacOS 下 Tun开启后 系统代理无法打开的问题 -- 修复服务模式启动时,修改、生成配置文件或重启内核可能导致页面卡死的问题 -- 修复 Webdav 恢复备份不重启 -- 修复 Linux 开机后无法正常代理需要手动设置 -- 修复增加订阅或导入订阅文件时订阅页面无更新 -- 修复系统代理守卫功能不工作 -- 修复 KDE + Wayland 下多屏显示 UI 异常 -- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 -- 修复静默启动不加载完整 WebView 的问题 -- 修复 Linux WebKit 网络进程的崩溃 -- 修复无法导入订阅 -- 修复实际导入成功但显示导入失败的问题 -- 修复服务不可用时,自动关闭 Tun 模式导致应用卡死问题 -- 修复删除订阅时未能实际删除相关文件 -- 修复 macOS 连接界面显示异常 -- 修复规则配置项在不同配置文件间全局共享导致切换被重置的问题 -- 修复 Linux Wayland 下部分 GPU 可能出现的 UI 渲染问题 -- 修复自动更新使版本回退的问题 -- 修复首页自定义卡片在切换轻量模式时失效 -- 修复悬浮跳转导航失效 -- 修复小键盘热键映射错误 -- 修复前端无法及时刷新操作状态 -- 修复 macOS 从 Dock 栏退出轻量模式状态不同步 -- 修复 Linux 系统主题切换不生效 -- 修复 `允许自动更新` 字段使手动订阅刷新失效 -- 修复轻量模式托盘状态不同步 -- 修复一键导入订阅导致应用卡死崩溃的问题 - -
- ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.15** -- 支持前端修改日志(最大文件大小、最大保留数量) -- 新增链式代理图形化设置功能 -- 新增系统标题栏与程序标题栏切换 (设置-页面设置-倾向系统标题栏) -- 监听关机事件,自动关闭系统代理 -- 主界面“当前节点”卡片新增“延迟测试”按钮 -- 新增批量选择配置文件功能 -- Windows / Linux / MacOS 监听关机信号,优雅恢复网络设置 -- 新增本地备份功能 -- 主界面“当前节点”卡片新增自动延迟检测开关(默认关闭) -- 允许独立控制订阅自动更新 -- 托盘 `更多` 中新增 `关闭所有连接` 按钮 -- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏) -- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志` -
- -
- 🚀 优化改进 - -- 重构并简化服务模式启动检测流程,消除重复检测 -- 重构并简化窗口创建流程 -- 重构日志系统,单个日志默认最大 10 MB -- 优化前端资源占用 -- 改进 macos 下系统代理设置的方法 -- 优化 TUN 模式可用性的判断 -- 移除流媒体检测的系统级提示(使用软件内通知) -- 优化后端 i18n 资源占用 -- 改进 Linux 托盘支持并添加 `--no-tray` 选项 -- Linux 现在在新生成的配置中默认将 TUN 栈恢复为 mixed 模式 -- 为代理延迟测试的 URL 设置增加了保护以及添加了安全的备用 URL -- 更新了 Wayland 合成器检测逻辑,从而在 Hyprland 会话中保留原生 Wayland 后端 -- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核 -- 修改内核默认日志级别为 Info -- 支持通过桌面快捷方式重新打开应用 -- 支持订阅界面输入链接后回车导入 -- 选择按延迟排序时每次延迟测试自动刷新节点顺序 -- 配置重载失败时自动重启核心 -- 启用 TUN 前等待服务就绪 -- 卸载 TUN 时会先关闭 -- 优化应用启动页 -- 优化首页当前节点对MATCH规则的支持 -- 允许在 `界面设置` 修改 `悬浮跳转导航延迟` -- 添加热键绑定错误的提示信息 -- 在 macOS 10.15 及更高版本默认包含 Mihomo-go122,以解决 Intel 架构 Mac 无法运行内核的问题 -- Tun 模式不可用时,禁用系统托盘的 Tun 模式菜单 -- 改进订阅更新方式,仍失败需打开订阅设置 `允许危险证书` -- 允许设置 Mihomo 端口范围 1000(含) - 65536(含) - -
- -## v2.4.2 - -### ✨ 新增功能 - -- 增加托盘节点选择 - -### 🚀 性能优化 - -- 优化前端首页加载速度 -- 优化前端未使用 i18n 文件缓存 -- 优化后端内存占用 -- 优化后端启动速度 - -### 🐞 修复问题 - -- 修复首页节点切换失效的问题 -- 修复和优化服务检查流程 -- 修复2.4.1引入的订阅地址重定向报错问题 -- 修复 rpm/deb 包名称问题 -- 修复托盘轻量模式状态检测异常 -- 修复通过 scheme 导入订阅崩溃 -- 修复单例检测实效 -- 修复启动阶段可能导致的无法连接内核 -- 修复导入订阅无法 Auth Basic - -### 👙 界面样式 - -- 简化和改进代理设置样式 - -## v2.4.1 - -### 🏆 重大改进 - -- **应用响应速度提升**:采用全新异步处理架构,大幅提升应用响应速度和稳定性 - -### ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.13** - -### 🚀 性能优化 - -- 优化热键响应速度,提升快捷键操作体验 -- 改进服务管理响应性,减少系统服务操作等待时间 -- 提升文件和配置处理性能 -- 优化任务管理和日志记录效率 -- 优化异步内存管理,减少内存占用并提升多任务处理效率 -- 优化启动阶段初始化性能 - -### 🐞 修复问题 - -- 修复应用在某些操作中可能出现的响应延迟问题 -- 修复任务管理中的潜在并发问题 -- 修复通过托盘重启应用无法恢复 -- 修复订阅在某些情况下无法导入 -- 修复无法新建订阅时使用远程链接 -- 修复卸载服务后的 tun 开关状态问题 -- 修复页面快速切换订阅时导致崩溃 -- 修复丢失工作目录时无法恢复环境 -- 修复从轻量模式恢复导致崩溃 - -### 👙 界面样式 - -- 统一代理设置样式 - -### 🗑️ 移除内容 - -- 移除启动阶段自动清理过期订阅 - -## v2.4.0 - -**发行代号:融** -代号释义: 「融」象征融合与贯通,寓意新版本通过全新 IPC 通信机制 将系统各部分紧密衔接,打破壁垒,实现更高效的 数据流通与全面性能优化。 - -### 🏆 重大改进 - -- **核心通信架构升级**:采用全新通信机制,提升应用性能和稳定性 -- **流量监控系统重构**:全新的流量监控界面,支持更丰富的数据展示 -- **数据缓存优化**:改进配置和节点数据缓存,提升响应速度 - -### ✨ 新增功能 - -- **Mihomo(Meta) 内核升级至 v1.19.12** -- 新增版本信息复制按钮 -- 增强型流量监控,支持更详细的数据分析 -- 新增流量图表多种显示模式 -- 新增强制刷新配置和节点缓存功能 -- 首页流量统计支持查看刻度线详情 - -### 🚀 性能优化 - -- 全面提升数据传输和处理效率 -- 优化内存使用,减少系统资源消耗 -- 改进流量图表渲染性能 -- 优化配置和节点刷新策略,从5秒延长到60秒 -- 改进数据缓存机制,减少重复请求 -- 优化异步程序性能 - -### 🐞 修复问题 - -- 修复系统代理状态检测和显示不一致问题 -- 修复系统主题窗口颜色不一致问题 -- 修复特殊字符 URL 处理问题 -- 修复配置修改后缓存不同步问题 -- 修复 Windows 安装器自启设置问题 -- 修复 macOS 下 Dock 图标恢复窗口问题 -- 修复 linux 下 KDE/Plasma 异常标题栏按钮 -- 修复架构升级后节点测速功能异常 -- 修复架构升级后流量统计功能异常 -- 修复架构升级后日志功能异常 -- 修复外部控制器跨域配置保存问题 -- 修复首页端口显示不一致问题 -- 修复首页流量统计刻度线显示问题 -- 修复日志页面按钮功能混淆问题 -- 修复日志等级设置保存问题 -- 修复日志等级异常过滤 -- 修复清理日志天数功能异常 -- 修复偶发性启动卡死问题 -- 修复首页虚拟网卡开关在管理模式下的状态问题 - -### 🔧 技术改进 - -- 统一使用新的内核通信方式 -- 新增外部控制器配置界面 -- 改进跨平台兼容性支持 - ## v2.3.2 ### 🐞 修复问题 diff --git a/package.json b/package.json index fadc52056..13523de0b 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "axios": "^1.13.2", "dayjs": "1.11.19", "foxact": "^0.2.49", - "i18next": "^25.6.1", + "i18next": "^25.6.2", "js-yaml": "^4.1.0", "json-schema": "^0.4.0", "lodash-es": "^4.17.21", @@ -71,7 +71,7 @@ "react-dom": "19.2.0", "react-error-boundary": "6.0.0", "react-hook-form": "^7.66.0", - "react-i18next": "16.2.4", + "react-i18next": "16.3.0", "react-markdown": "10.1.0", "react-router": "^7.9.5", "react-virtuoso": "^4.14.1", @@ -81,16 +81,16 @@ }, "devDependencies": { "@actions/github": "^6.0.1", - "@eslint-react/eslint-plugin": "^2.3.1", + "@eslint-react/eslint-plugin": "^2.3.4", "@eslint/js": "^9.39.1", - "@tauri-apps/cli": "2.9.3", + "@tauri-apps/cli": "2.9.4", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.10.0", - "@types/react": "19.2.2", - "@types/react-dom": "19.2.2", + "@types/node": "^24.10.1", + "@types/react": "19.2.3", + "@types/react-dom": "19.2.3", "@vitejs/plugin-legacy": "^7.2.1", - "@vitejs/plugin-react-swc": "^4.2.1", + "@vitejs/plugin-react-swc": "^4.2.2", "adm-zip": "^0.5.16", "cli-color": "^2.0.4", "commander": "^14.0.2", @@ -112,11 +112,11 @@ "meta-json-schema": "^1.19.14", "node-fetch": "^3.3.2", "prettier": "^3.6.2", - "sass": "^1.93.3", + "sass": "^1.94.0", "tar": "^7.5.2", "terser": "^5.44.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", + "typescript-eslint": "^8.46.4", "vite": "^7.2.2", "vite-plugin-monaco-editor-esm": "^2.0.2", "vite-plugin-svgr": "^4.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63720b241..c2bb098a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 3.2.2(react@19.2.0) '@emotion/react': specifier: ^11.14.0 - version: 11.14.0(@types/react@19.2.2)(react@19.2.0) + version: 11.14.0(@types/react@19.2.3)(react@19.2.0) '@emotion/styled': specifier: ^11.14.1 - version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) '@juggle/resize-observer': specifier: ^3.4.0 version: 3.4.0 @@ -31,16 +31,16 @@ importers: version: 4.7.0(monaco-editor@0.54.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/icons-material': specifier: ^7.3.5 - version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) '@mui/lab': specifier: 7.0.0-beta.17 - version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': specifier: ^7.3.5 - version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/x-data-grid': specifier: ^8.17.0 - version: 8.17.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 8.17.0(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': specifier: 2.9.0 version: 2.9.0 @@ -81,8 +81,8 @@ importers: specifier: ^0.2.49 version: 0.2.49(react@19.2.0) i18next: - specifier: ^25.6.1 - version: 25.6.1(typescript@5.9.3) + specifier: ^25.6.2 + version: 25.6.2(typescript@5.9.3) js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -114,11 +114,11 @@ importers: specifier: ^7.66.0 version: 7.66.0(react@19.2.0) react-i18next: - specifier: 16.2.4 - version: 16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + specifier: 16.3.0 + version: 16.3.0(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-markdown: specifier: 10.1.0 - version: 10.1.0(@types/react@19.2.2)(react@19.2.0) + version: 10.1.0(@types/react@19.2.3)(react@19.2.0) react-router: specifier: ^7.9.5 version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -130,7 +130,7 @@ importers: version: 2.3.6(react@19.2.0) tauri-plugin-mihomo-api: specifier: git+https://github.com/clash-verge-rev/tauri-plugin-mihomo - version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -139,14 +139,14 @@ importers: specifier: ^6.0.1 version: 6.0.1 '@eslint-react/eslint-plugin': - specifier: ^2.3.1 - version: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^2.3.4 + version: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@eslint/js': specifier: ^9.39.1 version: 9.39.1 '@tauri-apps/cli': - specifier: 2.9.3 - version: 2.9.3 + specifier: 2.9.4 + version: 2.9.4 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -154,20 +154,20 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.10.0 - version: 24.10.0 + specifier: ^24.10.1 + version: 24.10.1 '@types/react': - specifier: 19.2.2 - version: 19.2.2 + specifier: 19.2.3 + version: 19.2.3 '@types/react-dom': - specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.2) + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.3) '@vitejs/plugin-legacy': specifier: ^7.2.1 - version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + version: 7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitejs/plugin-react-swc': - specifier: ^4.2.1 - version: 4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + specifier: ^4.2.2 + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -188,10 +188,10 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-import-x: specifier: ^4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + version: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) @@ -203,7 +203,7 @@ importers: version: 0.4.24(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) glob: specifier: ^11.0.3 version: 11.0.3 @@ -232,8 +232,8 @@ importers: specifier: ^3.6.2 version: 3.6.2 sass: - specifier: ^1.93.3 - version: 1.93.3 + specifier: ^1.94.0 + version: 1.94.0 tar: specifier: ^7.5.2 version: 7.5.2 @@ -244,20 +244,20 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.46.4 + version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.2 - version: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) vite-plugin-monaco-editor-esm: specifier: ^2.0.2 version: 2.0.2(monaco-editor@0.54.0) vite-plugin-svgr: specifier: ^4.5.0 - version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) vitest: specifier: ^4.0.8 - version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + version: 4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) packages: @@ -1010,31 +1010,31 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@2.3.1': - resolution: {integrity: sha512-jB/P72HVbZcC7DtUvjna8tjPSageAS6L9x5muMsBRQxEXkfv2J6CPX47sSpaPu1mMJn1Zzpn9m5z4aTPbfV6Ug==} + '@eslint-react/ast@2.3.4': + resolution: {integrity: sha512-wueu2vBRwKqnrgOuz1JmEefhn5dgfa1NgvFtDMfefePfICTYFtwkTuOJNGJI77TUnqV9inp9ZNDB1nyAMmlZMg==} engines: {node: '>=20.19.0'} - '@eslint-react/core@2.3.1': - resolution: {integrity: sha512-R0gXjIqHqqYSeHxNMbXblnlwzmZ2gD32aVPmrJB+SrLP0rItzo/WgVSvstjOK5+N5KExdM87hopFcqnlZS3ONg==} + '@eslint-react/core@2.3.4': + resolution: {integrity: sha512-1ve3pSjAhd4PKcTpZd0bQwkAgWOAJjfgAixTfOvJ7QhWi3gPKq0Wn0vi78XcFO8K0IL3EcOfX2g2JhZJzSRnWQ==} engines: {node: '>=20.19.0'} - '@eslint-react/eff@2.3.1': - resolution: {integrity: sha512-k58lxHmhzatRZXVFzWdLZwfRwPgI5Thhf7BNVJ9F+NI2G1fFypclSVFRPqjGmI5jQ8bqB+5UVt9Rh49rZGZPzw==} + '@eslint-react/eff@2.3.4': + resolution: {integrity: sha512-/SXI23DpQCutSXPdAPdSvErfsBFYwaqbgQy5dLmnde4wHivk/eD9O/rA/Xu1c4j9tLJG/sZBQ9DzN/UXHizjag==} engines: {node: '>=20.19.0'} - '@eslint-react/eslint-plugin@2.3.1': - resolution: {integrity: sha512-ThWx+AWI3Tl/6g+L1Cq/kTQrrZ4NXWMxRN92iBswYMW7bPaolh/8WBdiLAVZldqnlm+l6LZriia89jyr0CeTHA==} + '@eslint-react/eslint-plugin@2.3.4': + resolution: {integrity: sha512-fcLZNQRbzjBK510a2oqLcsvdCSMK169x3CGfi1E3JvdMPyLICM+KZ8wNnhULZPey7BnEFVWC7dQfz5Dct60C7g==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - '@eslint-react/shared@2.3.1': - resolution: {integrity: sha512-UiTbPi1i7UPdsIT2Z7mKZ3zzrgAm1GLeexkKe4QwvZJ1LLeEJmgMwHUw852+VzlDeV8stcQmZ9zWqFX2L0CmGg==} + '@eslint-react/shared@2.3.4': + resolution: {integrity: sha512-FN3X4jA0kksFO9cjXwRaFHA6wbc7AgHjYkczdfwm0dOr/uUVv3Sc/F3kUNJ6KFOSoDCfNGybRCICu9DZbanhWA==} engines: {node: '>=20.19.0'} - '@eslint-react/var@2.3.1': - resolution: {integrity: sha512-1rC9dbuKKMq77pPoODGT91VTA3ReivIAfdFJePEjscPSRAUhCy7QPA/yK8MPe9nTsG89IDV+hilCGKiLZW8vNQ==} + '@eslint-react/var@2.3.4': + resolution: {integrity: sha512-hT2TvusBcEfFLFGfwZRstkQv9lp+079DEba5F6Dr53g3f+gBvLNDcayb5zDgwNP5+hQRd8lY8goEOfA8PWyaJQ==} engines: {node: '>=20.19.0'} '@eslint/config-array@0.21.1': @@ -1430,8 +1430,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@rolldown/pluginutils@1.0.0-beta.46': - resolution: {integrity: sha512-xMNwJo/pHkEP/mhNVnW+zUiJDle6/hxrwO0mfSJuEVRbBfgrJFuUSRoZx/nYUw5pCjrysl9OkNXCkAdih8GCnA==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} '@rollup/pluginutils@5.2.0': resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} @@ -1694,74 +1694,74 @@ packages: '@tauri-apps/api@2.9.0': resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==} - '@tauri-apps/cli-darwin-arm64@2.9.3': - resolution: {integrity: sha512-W8FQXZXQmQ0Fmj9UJXNrm2mLdIaLLriKVY7o/FzmizyIKTPIvHjfZALTNybbpTQRbJvKoGHLrW1DNzAWVDWJYg==} + '@tauri-apps/cli-darwin-arm64@2.9.4': + resolution: {integrity: sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.9.3': - resolution: {integrity: sha512-zDwu40rlshijt3TU6aRvzPUyVpapsx1sNfOlreDMTaMelQLHl6YoQzSRpLHYwrHrhimxyX2uDqnKIiuGel0Lhg==} + '@tauri-apps/cli-darwin-x64@2.9.4': + resolution: {integrity: sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.3': - resolution: {integrity: sha512-+Oc2OfcTRwYtW93VJqd/HOk77buORwC9IToj/qsEvM7bTMq6Kda4alpZprzwrCHYANSw+zD8PgjJdljTpe4p+g==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': + resolution: {integrity: sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.9.3': - resolution: {integrity: sha512-59GqU/J1n9wFyAtleoQOaU0oVIo+kwQynEw4meFDoKRXszKGor6lTsbsS3r0QKLSPbc0o/yYGJhqqCtkYjb/eg==} + '@tauri-apps/cli-linux-arm64-gnu@2.9.4': + resolution: {integrity: sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.9.3': - resolution: {integrity: sha512-fzvG+jEn5/iYGNH6Z2IRMheYFC4pJdXa19BR9fFm6Bdn2cuajRLDKdUcEME/DCtwqclphXtFZTrT4oezY5vI/A==} + '@tauri-apps/cli-linux-arm64-musl@2.9.4': + resolution: {integrity: sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-riscv64-gnu@2.9.3': - resolution: {integrity: sha512-qV8DZXI/fZwawk6T3Th1g6smiNC2KeQTk7XFgKvqZ6btC01z3UTsQmNGvI602zwm3Ld1TBZb4+rEWu2QmQimmw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': + resolution: {integrity: sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.9.3': - resolution: {integrity: sha512-tquyEONCNRfqEBWEe4eAHnxFN5yY5lFkCuD4w79XLIovUxVftQ684+xLp7zkhntkt4y20SMj2AgJa/+MOlx4Kg==} + '@tauri-apps/cli-linux-x64-gnu@2.9.4': + resolution: {integrity: sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.9.3': - resolution: {integrity: sha512-v2cBIB/6ji8DL+aiL5QUykU3ZO8OoJGyx50/qv2HQVzkf85KdaYSis3D/oVRemN/pcDz+vyCnnL3XnzFnDl4JQ==} + '@tauri-apps/cli-linux-x64-musl@2.9.4': + resolution: {integrity: sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.9.3': - resolution: {integrity: sha512-ZGvBy7nvrHPbE0HeKp/ioaiw8bNgAHxWnb7JRZ4/G0A+oFj0SeSFxl9k5uU6FKnM7bHM23Gd1oeaDex9g5Fceg==} + '@tauri-apps/cli-win32-arm64-msvc@2.9.4': + resolution: {integrity: sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.9.3': - resolution: {integrity: sha512-UsgIwOnpCoY9NK9/65QiwgmWVIE80LE7SwRYVblGtmlY9RYfsYvpbItwsovA/AcHMTiO+OCvS/q9yLeqS3m6Sg==} + '@tauri-apps/cli-win32-ia32-msvc@2.9.4': + resolution: {integrity: sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.9.3': - resolution: {integrity: sha512-fmw7NrrHE5m49idCvJAx9T9bsupjdJ0a3p3DPCNCZRGANU6R1tA1L+KTlVuUtdAldX2NqU/9UPo2SCslYKgJHQ==} + '@tauri-apps/cli-win32-x64-msvc@2.9.4': + resolution: {integrity: sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.9.3': - resolution: {integrity: sha512-BQ7iLUXTQcyG1PpzLWeVSmBCedYDpnA/6Cm/kRFGtqjTf/eVUlyYO5S2ee07tLum3nWwDBWTGFZeruO8yEukfA==} + '@tauri-apps/cli@2.9.4': + resolution: {integrity: sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw==} engines: {node: '>= 10'} hasBin: true @@ -1831,8 +1831,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.10.0': - resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1840,8 +1840,8 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-dom@19.2.2': - resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 @@ -1850,8 +1850,8 @@ packages: peerDependencies: '@types/react': '*' - '@types/react@19.2.2': - resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/react@19.2.3': + resolution: {integrity: sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1859,63 +1859,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.46.3': - resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.3 + '@typescript-eslint/parser': ^8.46.4 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.3': - resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.3': - resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.3': - resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.3': - resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.3': - resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.3': - resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.3': - resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.3': - resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.3': - resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -2023,8 +2023,8 @@ packages: terser: ^5.16.0 vite: ^7.0.0 - '@vitejs/plugin-react-swc@4.2.1': - resolution: {integrity: sha512-SIZ/XxeS2naLw4L2vVvpTyujM2OY+Rf+y6nWETqfoBrZpI3SFdyNJof3nQ8HbLhXJ1Eh9e9c0JGYC8GYPhLkCw==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 @@ -2613,15 +2613,15 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@2.3.1: - resolution: {integrity: sha512-Zuvb8iDYRbi8s7mYzvjHKD+i+loHjF6TKJiLGYM/t9F42OWU7V7b4sjIM7pXueukl0o8BSJXDVrQ+9sHOOmxBA==} + eslint-plugin-react-dom@2.3.4: + resolution: {integrity: sha512-xAetOWwrfYnqU0Vd6LjEQi1paAwU7Tgoa+jHUb7QCKxdILDV618yPrMigWpY+438cg6oXTYE/awFI/Rv+dOFCw==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-hooks-extra@2.3.1: - resolution: {integrity: sha512-2t4xQYhUEgPNq1SDQJEXuH3doT+h5spVmerX4rPnBFx0zG2sYfaJV1Gz6z40pI1L3CtBrZag5nFJ44AF/BEg0w==} + eslint-plugin-react-hooks-extra@2.3.4: + resolution: {integrity: sha512-widwgbMP3cC3sjmHidryg2McYqnzoIWcpsQ4JYnaXmRMMTem+Sp7a3+fkWvAUfWQBQIbigPT9O2+VhlvC+xlPg==} engines: {node: '>=20.0.0'} peerDependencies: eslint: ^9.38.0 @@ -2633,8 +2633,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react-naming-convention@2.3.1: - resolution: {integrity: sha512-Ghh1o++3XDk3zNKF7DXy3kIN1kJYFiH7wvl4aJF5m9LytQGFrJKTA5kygAaWgR7iL8o4mjk5Ty6Be3OKskpHwA==} + eslint-plugin-react-naming-convention@2.3.4: + resolution: {integrity: sha512-KP6d3WxiLmTnm245VK1lLI4Pzu/tkJLNOsmAetDm0kPfamD71msZMerp4VYFsFtqk0km9rbRKE7Cdd27tL43Ow==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -2645,15 +2645,15 @@ packages: peerDependencies: eslint: '>=8.40' - eslint-plugin-react-web-api@2.3.1: - resolution: {integrity: sha512-rb7AYR9SCJkCDkFdqnD6JHNLKF1o29o6tZLSaPdzA1Ssxh7/VKgJ8GpTrgl3Rv+Gnyn+w+3w4XE14d7T1Db9nA==} + eslint-plugin-react-web-api@2.3.4: + resolution: {integrity: sha512-NQMVDVIOiU+vJ98hkrIZeNdIwmO54UOjB8EfiHkgDWk69QBJrBtrn5AguFBuUlBUdZR/9vp4yKdQFtzBbRcDTw==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 typescript: ^5.9.3 - eslint-plugin-react-x@2.3.1: - resolution: {integrity: sha512-7zfi297NfkoEtqaz2W953gdK4J9nJD5okVhJVxgrcrP+9FVertkGqpbWtMZLpQuWJ216FncY8P6t1U+af8KNOA==} + eslint-plugin-react-x@2.3.4: + resolution: {integrity: sha512-6rW/CALxGCO9M2wuIUMNKuepSf5uW+Sg+AaQtLpsfs+B6fmDVHFrxIgg9wYk2aRrI6MMQcjDUMEYPUX9eOg2wQ==} engines: {node: '>=20.19.0'} peerDependencies: eslint: ^9.38.0 @@ -2953,8 +2953,8 @@ packages: engines: {node: '>=18'} hasBin: true - i18next@25.6.1: - resolution: {integrity: sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw==} + i18next@25.6.2: + resolution: {integrity: sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3251,9 +3251,6 @@ packages: lru-queue@0.1.0: resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3637,10 +3634,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@16.2.4: - resolution: {integrity: sha512-pvbcPQ+YuQQoRkKBA4VCU9aO8dOgP/vdKEizIYXcAk3+AmI8yQKSJaCzxQQu4Kgg2zWZm3ax9KqHv8ItUlRY0A==} + react-i18next@16.3.0: + resolution: {integrity: sha512-XGYIVU6gCOL4UQsfp87WbbvBc2WvgdkEDI8r4TwACzFg1bXY8pd1d9Cw6u9WJ2soTKHKaF1xQEyWA3/dUvtAGw==} peerDependencies: - i18next: '>= 25.5.2' + i18next: '>= 25.6.2' react: '>= 16.8.0' react-dom: '*' react-native: '*' @@ -3779,8 +3776,8 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass@1.93.3: - resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} + sass@1.94.0: + resolution: {integrity: sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -3981,8 +3978,8 @@ packages: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c: - resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39} version: 0.1.0 terser@5.44.1: @@ -4068,8 +4065,8 @@ packages: types-pac@1.0.3: resolution: {integrity: sha512-MF2UAZGvGMOM+vHi9Zj/LvQqdNN1m1xSB+PjAW9B/GvFqaB4GwR18YaIbGIGDRTW/J8iqFXQHLZd5eJVtho46w==} - typescript-eslint@8.46.3: - resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} + typescript-eslint@8.46.4: + resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -5080,7 +5077,7 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0)': + '@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 @@ -5092,7 +5089,7 @@ snapshots: hoist-non-react-statics: 3.3.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 transitivePeerDependencies: - supports-color @@ -5106,18 +5103,18 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 transitivePeerDependencies: - supports-color @@ -5215,27 +5212,27 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/ast@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) string-ts: 2.2.1 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/core@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/core@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) birecord: 0.1.1 ts-pattern: 5.9.0 transitivePeerDependencies: @@ -5243,31 +5240,31 @@ snapshots: - supports-color - typescript - '@eslint-react/eff@2.3.1': {} + '@eslint-react/eff@2.3.4': {} - '@eslint-react/eslint-plugin@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/eslint-plugin@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-dom: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-hooks-extra: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-naming-convention: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-web-api: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-react-x: 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-dom: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-hooks-extra: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-naming-convention: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-web-api: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-react-x: 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/shared@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/eff': 2.3.1 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 zod: 4.1.12 transitivePeerDependencies: @@ -5275,13 +5272,13 @@ snapshots: - supports-color - typescript - '@eslint-react/var@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@eslint-react/var@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) ts-pattern: 5.9.0 transitivePeerDependencies: - eslint @@ -5400,39 +5397,39 @@ snapshots: '@mui/core-downloads-tracker@7.3.5': {} - '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/lab@7.0.0-beta.17(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@mui/core-downloads-tracker': 7.3.5 - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) '@popperjs/core': 2.11.8 - '@types/react-transition-group': 4.4.12(@types/react@19.2.2) + '@types/react-transition-group': 4.4.12(@types/react@19.2.3) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -5441,20 +5438,20 @@ snapshots: react-is: 19.2.0 react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/private-theming@7.3.5(@types/react@19.2.2)(react@19.2.0)': + '@mui/private-theming@7.3.5(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0)': + '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/cache': 11.14.0 @@ -5464,77 +5461,77 @@ snapshots: prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) - '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)': + '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/private-theming': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(react@19.2.0) - '@mui/types': 7.4.8(@types/react@19.2.2) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/private-theming': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(react@19.2.0) + '@mui/types': 7.4.8(@types/react@19.2.3) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 react: 19.2.0 optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@types/react': 19.2.2 + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@types/react': 19.2.3 - '@mui/types@7.4.8(@types/react@19.2.2)': + '@mui/types@7.4.8(@types/react@19.2.3)': dependencies: '@babel/runtime': 7.28.4 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/utils@7.3.5(@types/react@19.2.2)(react@19.2.0)': + '@mui/utils@7.3.5(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/types': 7.4.8(@types/react@19.2.2) + '@mui/types': 7.4.8(@types/react@19.2.3) '@types/prop-types': 15.7.15 clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-is: 19.2.0 optionalDependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@mui/x-data-grid@8.17.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-data-grid@8.17.0(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/x-internals': 8.17.0(@types/react@19.2.2)(react@19.2.0) - '@mui/x-virtualizer': 0.2.7(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/x-internals': 8.17.0(@types/react@19.2.3)(react@19.2.0) + '@mui/x-virtualizer': 0.2.7(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: - '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) + '@emotion/react': 11.14.0(@types/react@19.2.3)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.3)(react@19.2.0))(@types/react@19.2.3)(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.17.0(@types/react@19.2.2)(react@19.2.0)': + '@mui/x-internals@8.17.0(@types/react@19.2.3)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) react: 19.2.0 reselect: 5.1.1 use-sync-external-store: 1.6.0(react@19.2.0) transitivePeerDependencies: - '@types/react' - '@mui/x-virtualizer@0.2.7(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@mui/x-virtualizer@0.2.7(@types/react@19.2.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 - '@mui/utils': 7.3.5(@types/react@19.2.2)(react@19.2.0) - '@mui/x-internals': 8.17.0(@types/react@19.2.2)(react@19.2.0) + '@mui/utils': 7.3.5(@types/react@19.2.3)(react@19.2.0) + '@mui/x-internals': 8.17.0(@types/react@19.2.3)(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -5682,7 +5679,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rolldown/pluginutils@1.0.0-beta.46': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} '@rollup/pluginutils@5.2.0(rollup@4.46.2)': dependencies: @@ -5881,52 +5878,52 @@ snapshots: '@tauri-apps/api@2.9.0': {} - '@tauri-apps/cli-darwin-arm64@2.9.3': + '@tauri-apps/cli-darwin-arm64@2.9.4': optional: true - '@tauri-apps/cli-darwin-x64@2.9.3': + '@tauri-apps/cli-darwin-x64@2.9.4': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.9.3': + '@tauri-apps/cli-linux-arm-gnueabihf@2.9.4': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.9.3': + '@tauri-apps/cli-linux-arm64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.9.3': + '@tauri-apps/cli-linux-arm64-musl@2.9.4': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.9.3': + '@tauri-apps/cli-linux-riscv64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.9.3': + '@tauri-apps/cli-linux-x64-gnu@2.9.4': optional: true - '@tauri-apps/cli-linux-x64-musl@2.9.3': + '@tauri-apps/cli-linux-x64-musl@2.9.4': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.9.3': + '@tauri-apps/cli-win32-arm64-msvc@2.9.4': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.9.3': + '@tauri-apps/cli-win32-ia32-msvc@2.9.4': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.9.3': + '@tauri-apps/cli-win32-x64-msvc@2.9.4': optional: true - '@tauri-apps/cli@2.9.3': + '@tauri-apps/cli@2.9.4': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.9.3 - '@tauri-apps/cli-darwin-x64': 2.9.3 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.3 - '@tauri-apps/cli-linux-arm64-gnu': 2.9.3 - '@tauri-apps/cli-linux-arm64-musl': 2.9.3 - '@tauri-apps/cli-linux-riscv64-gnu': 2.9.3 - '@tauri-apps/cli-linux-x64-gnu': 2.9.3 - '@tauri-apps/cli-linux-x64-musl': 2.9.3 - '@tauri-apps/cli-win32-arm64-msvc': 2.9.3 - '@tauri-apps/cli-win32-ia32-msvc': 2.9.3 - '@tauri-apps/cli-win32-x64-msvc': 2.9.3 + '@tauri-apps/cli-darwin-arm64': 2.9.4 + '@tauri-apps/cli-darwin-x64': 2.9.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.9.4 + '@tauri-apps/cli-linux-arm64-musl': 2.9.4 + '@tauri-apps/cli-linux-riscv64-gnu': 2.9.4 + '@tauri-apps/cli-linux-x64-gnu': 2.9.4 + '@tauri-apps/cli-linux-x64-musl': 2.9.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.9.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.9.4 + '@tauri-apps/cli-win32-x64-msvc': 2.9.4 '@tauri-apps/plugin-clipboard-manager@2.3.2': dependencies: @@ -6002,7 +5999,7 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.10.0': + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 @@ -6010,15 +6007,15 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@19.2.2(@types/react@19.2.2)': + '@types/react-dom@19.2.3(@types/react@19.2.3)': dependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@types/react-transition-group@4.4.12(@types/react@19.2.2)': + '@types/react-transition-group@4.4.12(@types/react@19.2.3)': dependencies: - '@types/react': 19.2.2 + '@types/react': 19.2.3 - '@types/react@19.2.2': + '@types/react@19.2.3': dependencies: csstype: 3.1.3 @@ -6026,14 +6023,14 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -6043,41 +6040,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.3': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 - '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -6085,14 +6082,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.3': {} + '@typescript-eslint/types@8.46.4': {} - '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -6103,20 +6100,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.8.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.3': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -6180,7 +6177,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-legacy@7.2.1(terser@5.44.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4) @@ -6191,19 +6188,19 @@ snapshots: browserslist: 4.25.1 browserslist-to-esbuild: 2.1.1(browserslist@4.25.1) core-js: 3.45.0 - magic-string: 0.30.19 + magic-string: 0.30.21 regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.44.1 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react-swc@4.2.1(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.46 + '@rolldown/pluginutils': 1.0.0-beta.47 '@swc/core': 1.14.0 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - '@swc/helpers' @@ -6216,13 +6213,13 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1))': + '@vitest/mocker@4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) '@vitest/pretty-format@4.0.8': dependencies: @@ -6845,7 +6842,7 @@ snapshots: - supports-color optional: true - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) @@ -6856,26 +6853,26 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color optional: true - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/types': 8.46.4 comment-parser: 1.4.1 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) @@ -6886,12 +6883,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6902,7 +6899,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -6914,7 +6911,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -6930,16 +6927,16 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1)) - eslint-plugin-react-dom@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-dom@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 @@ -6948,17 +6945,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-hooks-extra@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -6977,17 +6974,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-naming-convention@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -6999,16 +6996,16 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-react-web-api@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-web-api@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) string-ts: 2.2.1 ts-pattern: 5.9.0 @@ -7016,17 +7013,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-x@2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-react-x@2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-react/ast': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/core': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/eff': 2.3.1 - '@eslint-react/shared': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@eslint-react/var': 2.3.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/ast': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/core': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/eff': 2.3.4 + '@eslint-react/shared': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@eslint-react/var': 2.3.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) compare-versions: 6.1.1 eslint: 9.39.1(jiti@2.6.1) is-immutable-type: 5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) @@ -7037,11 +7034,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -7379,7 +7376,7 @@ snapshots: husky@9.1.7: {} - i18next@25.6.1(typescript@5.9.3): + i18next@25.6.2(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 optionalDependencies: @@ -7500,7 +7497,7 @@ snapshots: is-immutable-type@5.0.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) ts-declaration-location: 1.0.7(typescript@5.9.3) @@ -7686,10 +7683,6 @@ snapshots: dependencies: es5-ext: 0.10.64 - magic-string@0.30.19: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -8207,11 +8200,11 @@ snapshots: dependencies: react: 19.2.0 - react-i18next@16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + react-i18next@16.3.0(i18next@25.6.2(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 html-parse-stringify: 3.0.1 - i18next: 25.6.1(typescript@5.9.3) + i18next: 25.6.2(typescript@5.9.3) react: 19.2.0 use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: @@ -8222,11 +8215,11 @@ snapshots: react-is@19.2.0: {} - react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.0): + react-markdown@10.1.0(@types/react@19.2.3)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.2.2 + '@types/react': 19.2.3 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -8403,7 +8396,7 @@ snapshots: is-regex: 1.2.1 optional: true - sass@1.93.3: + sass@1.94.0: dependencies: chokidar: 4.0.3 immutable: 5.1.2 @@ -8636,7 +8629,7 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/dcb6b5a6753233422e7cea23042239c7994c605c: + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/d0f00b33cea294cc693e177441fc897426ecbc39: dependencies: '@tauri-apps/api': 2.9.0 @@ -8739,12 +8732,12 @@ snapshots: types-pac@1.0.3: {} - typescript-eslint@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -8864,18 +8857,18 @@ snapshots: dependencies: monaco-editor: 0.54.0 - vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@rollup/pluginutils': 5.2.0(rollup@4.46.2) '@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1): + vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: esbuild: 0.25.4 fdir: 6.5.0(picomatch@4.0.3) @@ -8884,17 +8877,17 @@ snapshots: rollup: 4.46.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.0 + '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.6.1 - sass: 1.93.3 + sass: 1.94.0 terser: 5.44.1 yaml: 2.8.1 - vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1): + vitest@4.0.8(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.8 - '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1)) + '@vitest/mocker': 4.0.8(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.8 '@vitest/runner': 4.0.8 '@vitest/snapshot': 4.0.8 @@ -8911,11 +8904,11 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.2.2(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.1)(yaml@2.8.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.0 + '@types/node': 24.10.1 transitivePeerDependencies: - jiti - less diff --git a/scripts/extract_update_logs.sh b/scripts/extract_update_logs.sh new file mode 100755 index 000000000..1283cf3b0 --- /dev/null +++ b/scripts/extract_update_logs.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# extract_update_logs.sh +# 从 Changelog.md 提取最新版本 (## v...) 的更新内容 +# 并输出到屏幕或写入环境变量文件(如 GitHub Actions) + +set -euo pipefail + +CHANGELOG_FILE="Changelog.md" + +if [[ ! -f "$CHANGELOG_FILE" ]]; then + echo "❌ 文件不存在: $CHANGELOG_FILE" >&2 + exit 1 +fi + +# 提取从第一个 '## v' 开始到下一个 '## v' 前的内容 +UPDATE_LOGS=$(awk ' + /^## v/ { + if (found) exit; + found=1 + } + found +' "$CHANGELOG_FILE") + +if [[ -z "$UPDATE_LOGS" ]]; then + echo "⚠️ 未找到更新日志内容" + exit 0 +fi + +echo "✅ 提取到的最新版本日志内容如下:" +echo "----------------------------------------" +echo "$UPDATE_LOGS" +echo "----------------------------------------" + +# 如果在 GitHub Actions 环境中(GITHUB_ENV 已定义) +if [[ -n "${GITHUB_ENV:-}" ]]; then + { + echo "UPDATE_LOGS<> "$GITHUB_ENV" + echo "✅ 已写入 GitHub 环境变量 UPDATE_LOGS" +fi diff --git a/scripts/updatelog.mjs b/scripts/updatelog.mjs index b00d35087..89faeabd7 100644 --- a/scripts/updatelog.mjs +++ b/scripts/updatelog.mjs @@ -2,9 +2,9 @@ import fs from "fs"; import fsp from "fs/promises"; import path from "path"; -const UPDATE_LOG = "UPDATELOG.md"; +const UPDATE_LOG = "Changelog.md"; -// parse the UPDATELOG.md +// parse the Changelog.md export async function resolveUpdateLog(tag) { const cwd = process.cwd(); @@ -14,7 +14,7 @@ export async function resolveUpdateLog(tag) { const file = path.join(cwd, UPDATE_LOG); if (!fs.existsSync(file)) { - throw new Error("could not found UPDATELOG.md"); + throw new Error("could not found Changelog.md"); } const data = await fsp.readFile(file, "utf-8"); @@ -38,7 +38,7 @@ export async function resolveUpdateLog(tag) { }); if (!map[tag]) { - throw new Error(`could not found "${tag}" in UPDATELOG.md`); + throw new Error(`could not found "${tag}" in Changelog.md`); } return map[tag].join("\n").trim(); @@ -49,7 +49,7 @@ export async function resolveUpdateLogDefault() { const file = path.join(cwd, UPDATE_LOG); if (!fs.existsSync(file)) { - throw new Error("could not found UPDATELOG.md"); + throw new Error("could not found Changelog.md"); } const data = await fsp.readFile(file, "utf-8"); @@ -77,7 +77,7 @@ export async function resolveUpdateLogDefault() { } if (!firstTag) { - throw new Error("could not found any version tag in UPDATELOG.md"); + throw new Error("could not found any version tag in Changelog.md"); } return content.join("\n").trim(); diff --git a/scripts/updater-fixed-webview2.mjs b/scripts/updater-fixed-webview2.mjs index 083c9b21f..33389c634 100644 --- a/scripts/updater-fixed-webview2.mjs +++ b/scripts/updater-fixed-webview2.mjs @@ -1,4 +1,4 @@ -import { getOctokit, context } from "@actions/github"; +import { context, getOctokit } from "@actions/github"; import fetch from "node-fetch"; import { resolveUpdateLog } from "./updatelog.mjs"; @@ -36,7 +36,7 @@ async function resolveUpdater() { const updateData = { name: tag.name, - notes: await resolveUpdateLog(tag.name), // use updatelog.md + notes: await resolveUpdateLog(tag.name), // use Changelog.md pub_date: new Date().toISOString(), platforms: { "windows-x86_64": { signature: "", url: "" }, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0793b48a5..798f57ace 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1258,7 +1258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3396,7 +3396,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -3416,7 +3416,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -5031,7 +5031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5831,7 +5831,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -5868,7 +5868,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -6417,7 +6417,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7285,7 +7285,7 @@ dependencies = [ [[package]] name = "sysproxy" version = "0.3.1" -source = "git+https://github.com/clash-verge-rev/sysproxy-rs#50100ab03eb802056c381f3c5009e903c67e3bac" +source = "git+https://github.com/clash-verge-rev/sysproxy-rs#ea6e5b5bcef32025e1df914d663eea8558afacb2" dependencies = [ "interfaces", "iptools", @@ -7704,7 +7704,7 @@ dependencies = [ [[package]] name = "tauri-plugin-mihomo" version = "0.1.1" -source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#dcb6b5a6753233422e7cea23042239c7994c605c" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#d0f00b33cea294cc693e177441fc897426ecbc39" dependencies = [ "base64 0.22.1", "futures-util", @@ -9348,7 +9348,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 242c6252c..65fc7abcc 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git" default-run = "clash-verge" edition = "2024" build = "build.rs" +rust-version = "1.91" [package.metadata.bundle] identifier = "io.github.clash-verge-rev.clash-verge-rev" @@ -27,10 +28,10 @@ sysinfo = { version = "0.37.2", features = ["network", "system"] } boa_engine = "0.21.0" serde_json = "1.0.145" serde_yaml_ng = "0.10.0" -once_cell = "1.21.3" +once_cell = { version = "1.21.3", features = ["parking_lot"] } port_scanner = "0.1.5" delay_timer = "0.11.6" -parking_lot = "0.12.5" +parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } percent-encoding = "2.3.2" tokio = { version = "1.48.0", features = [ "rt-multi-thread", @@ -186,8 +187,10 @@ unimplemented = "deny" # Development quality lints todo = "warn" dbg_macro = "warn" -#print_stdout = "warn" -#print_stderr = "warn" + +# 我们期望所有输出方式通过 logging 模块进行统一管理 +# print_stdout = "deny" +# print_stderr = "deny" # Performance lints for proxy application clone_on_ref_ptr = "warn" @@ -245,3 +248,14 @@ iter_on_empty_collections = "deny" equatable_if_let = "deny" collection_is_never_read = "deny" branches_sharing_code = "deny" +pathbuf_init_then_push = "deny" +option_as_ref_cloned = "deny" +large_types_passed_by_value = "deny" +# implicit_clone = "deny" // 可能会造成额外开销,暂时不开启 +expl_impl_clone_on_copy = "deny" +copy_iterator = "deny" +cloned_instead_of_copied = "deny" +# self_only_used_in_recursion = "deny" // Since 1.92.0 +unnecessary_self_imports = "deny" +unused_trait_names = "deny" +wildcard_imports = "deny" diff --git a/src-tauri/locales/ar.yml b/src-tauri/locales/ar.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/ar.yml +++ b/src-tauri/locales/ar.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/de.yml b/src-tauri/locales/de.yml index 330cf3b50..2cda8afe8 100644 --- a/src-tauri/locales/de.yml +++ b/src-tauri/locales/de.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Regel + direct: Direkt + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/en.yml b/src-tauri/locales/en.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/en.yml +++ b/src-tauri/locales/en.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/es.yml b/src-tauri/locales/es.yml index 330cf3b50..974899b15 100644 --- a/src-tauri/locales/es.yml +++ b/src-tauri/locales/es.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Regla + direct: Directo + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/fa.yml b/src-tauri/locales/fa.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/fa.yml +++ b/src-tauri/locales/fa.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/id.yml b/src-tauri/locales/id.yml index 330cf3b50..587534e9c 100644 --- a/src-tauri/locales/id.yml +++ b/src-tauri/locales/id.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Aturan + direct: Langsung + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/jp.yml b/src-tauri/locales/jp.yml index 330cf3b50..4ed031a4a 100644 --- a/src-tauri/locales/jp.yml +++ b/src-tauri/locales/jp.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: ルール + direct: ダイレクト + global: グローバル profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/ko.yml b/src-tauri/locales/ko.yml index b671b8aec..51dccf885 100644 --- a/src-tauri/locales/ko.yml +++ b/src-tauri/locales/ko.yml @@ -28,6 +28,10 @@ tray: ruleMode: 규칙 모드 globalMode: 전역 모드 directMode: 직접 모드 + outboundModes: Outbound Modes + rule: 규칙 + direct: 직접 + global: 글로벌 profiles: 프로필 proxies: 프록시 systemProxy: 시스템 프록시 diff --git a/src-tauri/locales/ru.yml b/src-tauri/locales/ru.yml index 330cf3b50..cff8fe27c 100644 --- a/src-tauri/locales/ru.yml +++ b/src-tauri/locales/ru.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Правило + direct: Прямой + global: Глобальный profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/tr.yml b/src-tauri/locales/tr.yml index 330cf3b50..f6ffb6895 100644 --- a/src-tauri/locales/tr.yml +++ b/src-tauri/locales/tr.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Kural + direct: Doğrudan + global: Küresel profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/tt.yml b/src-tauri/locales/tt.yml index 330cf3b50..a054627f7 100644 --- a/src-tauri/locales/tt.yml +++ b/src-tauri/locales/tt.yml @@ -28,6 +28,10 @@ tray: ruleMode: Rule Mode globalMode: Global Mode directMode: Direct Mode + outboundModes: Outbound Modes + rule: Rule + direct: Direct + global: Global profiles: Profiles proxies: Proxies systemProxy: System Proxy diff --git a/src-tauri/locales/zh.yml b/src-tauri/locales/zh.yml index 10c2e6362..6b137faeb 100644 --- a/src-tauri/locales/zh.yml +++ b/src-tauri/locales/zh.yml @@ -28,6 +28,10 @@ tray: ruleMode: 规则模式 globalMode: 全局模式 directMode: 直连模式 + outboundModes: 出站模式 + rule: 规则 + direct: 直连 + global: 全局 profiles: 订阅 proxies: 代理 systemProxy: 系统代理 diff --git a/src-tauri/locales/zhtw.yml b/src-tauri/locales/zhtw.yml index 040226b2e..9e07ca714 100644 --- a/src-tauri/locales/zhtw.yml +++ b/src-tauri/locales/zhtw.yml @@ -10,8 +10,8 @@ notifications: title: 系統代理 body: 系統代理狀態已更新。 tunModeToggled: - title: TUN 模式 - body: TUN 模式狀態已更新。 + title: 虛擬網路介面卡模式 + body: 已更新虛擬網路介面卡模式狀態。 lightweightModeEntered: title: 輕量模式 body: 已進入輕量模式。 @@ -28,10 +28,14 @@ tray: ruleMode: 規則模式 globalMode: 全域模式 directMode: 直連模式 + outboundModes: 出站模式 + rule: 規則 + direct: 直連 + global: 全域 profiles: 訂閱 proxies: 代理 systemProxy: 系統代理 - tunMode: TUN 模式 + tunMode: 虛擬網路介面卡模式 closeAllConnections: 關閉所有連線 lightweightMode: 輕量模式 copyEnv: 複製環境變數 @@ -48,5 +52,5 @@ tray: exit: 離開 tooltip: systemProxy: 系統代理 - tun: TUN + tun: 虛擬網路介面卡 profile: 訂閱 diff --git a/src-tauri/rust-toolchain.toml b/src-tauri/rust-toolchain.toml new file mode 100644 index 000000000..cdeba7a2b --- /dev/null +++ b/src-tauri/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.91.0" +components = ["rustfmt", "clippy"] diff --git a/src-tauri/rustfmt.toml b/src-tauri/rustfmt.toml index baaa750e3..7e141a0b8 100644 --- a/src-tauri/rustfmt.toml +++ b/src-tauri/rustfmt.toml @@ -6,7 +6,7 @@ use_small_heuristics = "Default" reorder_imports = true reorder_modules = true remove_nested_parens = true -edition = "2021" +edition = "2024" merge_derives = true use_try_shorthand = false use_field_init_shorthand = false diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index 959277feb..4a806acb7 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -1,18 +1,19 @@ use super::CmdResult; use crate::core::sysopt::Sysopt; +use crate::utils::resolve::ui::{self, UiReadyStage}; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, feat, logging, utils::{ - dirs::{self, PathBufExec}, + dirs::{self, PathBufExec as _}, logging::Type, }, }; use smartstring::alias::String; use std::path::Path; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager as _}; use tokio::fs; -use tokio::io::AsyncWriteExt; +use tokio::io::AsyncWriteExt as _; /// 打开应用程序所在目录 #[tauri::command] @@ -242,34 +243,14 @@ pub async fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult CmdResult<()> { logging!(info, Type::Cmd, "前端UI已准备就绪"); - crate::utils::resolve::ui::mark_ui_ready(); + ui::mark_ui_ready(); Ok(()) } /// UI加载阶段 #[tauri::command] -pub fn update_ui_stage(stage: String) -> CmdResult<()> { - logging!(info, Type::Cmd, "UI加载阶段更新: {}", stage.as_str()); - - use crate::utils::resolve::ui::UiReadyStage; - - let stage_enum = match stage.as_str() { - "NotStarted" => UiReadyStage::NotStarted, - "Loading" => UiReadyStage::Loading, - "DomReady" => UiReadyStage::DomReady, - "ResourcesLoaded" => UiReadyStage::ResourcesLoaded, - "Ready" => UiReadyStage::Ready, - _ => { - logging!( - warn, - Type::Cmd, - "Warning: 未知的UI加载阶段: {}", - stage.as_str() - ); - return Err(format!("未知的UI加载阶段: {}", stage.as_str()).into()); - } - }; - - crate::utils::resolve::ui::update_ui_ready_stage(stage_enum); +pub fn update_ui_stage(stage: UiReadyStage) -> CmdResult<()> { + logging!(info, Type::Cmd, "UI加载阶段更新: {:?}", &stage); + ui::update_ui_ready_stage(stage); Ok(()) } diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs index a105f4d8a..238e81bdc 100644 --- a/src-tauri/src/cmd/backup.rs +++ b/src-tauri/src/cmd/backup.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, feat}; +use crate::{cmd::StringifyErr as _, feat}; use feat::LocalBackupFile; use smartstring::alias::String; diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 84855bd5e..ab37a4b3d 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,7 +1,7 @@ use super::CmdResult; use crate::utils::dirs; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, config::{ClashInfo, Config}, constants, core::{CoreManager, handle, validate::CoreConfigValidator}, @@ -22,7 +22,7 @@ pub async fn copy_clash_env() -> CmdResult { /// 获取Clash信息 #[tauri::command] pub async fn get_clash_info() -> CmdResult { - Ok(Config::clash().await.latest_arc().get_client_info()) + Ok(Config::clash().await.data_arc().get_client_info()) } /// 修改Clash配置 diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index c0dedfde6..be2fccff8 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::cmd::StringifyErr; +use crate::cmd::StringifyErr as _; use crate::core::{EventDrivenProxyManager, async_proxy_query::AsyncProxyQuery}; use crate::process::AsyncHandler; use crate::{logging, utils::logging::Type}; @@ -93,7 +93,7 @@ pub fn get_network_interfaces() -> Vec { /// 获取网络接口详细信息 #[tauri::command] pub fn get_network_interfaces_info() -> CmdResult> { - use network_interface::{NetworkInterface, NetworkInterfaceConfig}; + use network_interface::{NetworkInterface, NetworkInterfaceConfig as _}; let names = get_network_interfaces(); let interfaces = NetworkInterface::show().stringify_err()?; diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 0368d520e..edb94795a 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use super::StringifyErr; +use super::StringifyErr as _; use crate::{ config::{ Config, IProfiles, PrfItem, PrfOption, @@ -11,6 +11,7 @@ use crate::{ }, core::{CoreManager, handle, timer::Timer, tray::Tray}, feat, logging, + module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, process::AsyncHandler, ret_err, utils::{dirs, help, logging::Type}, @@ -90,6 +91,7 @@ pub async fn import_profile(url: std::string::String, option: Option) } logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } @@ -122,6 +124,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResu handle::Handle::notify_profile_changed(uid.clone()); } Config::profiles().await.apply(); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } Err(err) => { @@ -164,6 +167,7 @@ pub async fn delete_profile(index: String) -> CmdResult { // 发送配置变更通知 logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index); handle::Handle::notify_profile_changed(index); + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); } Err(e) => { logging!(error, Type::Cmd, "{}", e); @@ -460,6 +464,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult { }); } + AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange); Ok(()) } diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index 5ecfe954f..d75e470d0 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,6 +1,11 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::*, core::CoreManager, log_err}; -use anyhow::{Context, anyhow}; +use crate::{ + cmd::StringifyErr as _, + config::{Config, ConfigType}, + core::CoreManager, + log_err, +}; +use anyhow::{Context as _, anyhow}; use serde_yaml_ng::Mapping; use smartstring::alias::String; use std::collections::HashMap; diff --git a/src-tauri/src/cmd/save_profile.rs b/src-tauri/src/cmd/save_profile.rs index a52462950..ed271504b 100644 --- a/src-tauri/src/cmd/save_profile.rs +++ b/src-tauri/src/cmd/save_profile.rs @@ -1,9 +1,10 @@ use super::CmdResult; use crate::{ - cmd::StringifyErr, - config::*, - core::{validate::CoreConfigValidator, *}, + cmd::StringifyErr as _, + config::{Config, PrfItem}, + core::{CoreManager, handle, validate::CoreConfigValidator}, logging, + module::auto_backup::{AutoBackupManager, AutoBackupTrigger}, utils::{dirs, logging::Type}, }; use smartstring::alias::String; @@ -17,6 +18,12 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR None => return Ok(()), }; + let backup_trigger = match index.as_str() { + "Merge" => Some(AutoBackupTrigger::GlobalMerge), + "Script" => Some(AutoBackupTrigger::GlobalScript), + _ => Some(AutoBackupTrigger::ProfileChange), + }; + // 在异步操作前获取必要元数据并释放锁 let (rel_path, is_merge_file) = { let profiles = Config::profiles().await; @@ -51,11 +58,17 @@ pub async fn save_profile_file(index: String, file_data: Option) -> CmdR is_merge_file ); - if is_merge_file { - return handle_merge_file(&file_path_str, &file_path, &original_content).await; + let changes_applied = if is_merge_file { + handle_merge_file(&file_path_str, &file_path, &original_content).await? + } else { + handle_full_validation(&file_path_str, &file_path, &original_content).await? + }; + + if changes_applied && let Some(trigger) = backup_trigger { + AutoBackupManager::trigger_backup(trigger); } - handle_full_validation(&file_path_str, &file_path, &original_content).await + Ok(()) } async fn restore_original( @@ -76,7 +89,7 @@ async fn handle_merge_file( file_path_str: &str, file_path: &std::path::Path, original_content: &str, -) -> CmdResult { +) -> CmdResult { logging!( info, Type::Config, @@ -96,7 +109,7 @@ async fn handle_merge_file( } else { handle::Handle::refresh_clash(); } - Ok(()) + Ok(true) } Ok((false, error_msg)) => { logging!( @@ -108,7 +121,7 @@ async fn handle_merge_file( restore_original(file_path, original_content).await?; let result = (false, error_msg.clone()); crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件"); - Ok(()) + Ok(false) } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); @@ -122,11 +135,11 @@ async fn handle_full_validation( file_path_str: &str, file_path: &std::path::Path, original_content: &str, -) -> CmdResult { +) -> CmdResult { match CoreConfigValidator::validate_config_file(file_path_str, None).await { Ok((true, _)) => { logging!(info, Type::Config, "[cmd配置save] 验证成功"); - Ok(()) + Ok(true) } Ok((false, error_msg)) => { logging!(warn, Type::Config, "[cmd配置save] 验证失败: {}", error_msg); @@ -160,7 +173,7 @@ async fn handle_full_validation( handle::Handle::notice_message("config_validate::error", error_msg.to_owned()); } - Ok(()) + Ok(false) } Err(e) => { logging!(error, Type::Config, "[cmd配置save] 验证过程发生错误: {}", e); diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 7e5b7d2c0..3f7b92273 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -1,4 +1,4 @@ -use super::{CmdResult, StringifyErr}; +use super::{CmdResult, StringifyErr as _}; use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus}; use smartstring::SmartString; diff --git a/src-tauri/src/cmd/system.rs b/src-tauri/src/cmd/system.rs index 8ba1e6c67..27a4f5f6c 100644 --- a/src-tauri/src/cmd/system.rs +++ b/src-tauri/src/cmd/system.rs @@ -10,7 +10,7 @@ use crate::{ #[cfg(target_os = "windows")] use deelevate::{PrivilegeLevel, Token}; use once_cell::sync::Lazy; -use tauri_plugin_clipboard_manager::ClipboardExt; +use tauri_plugin_clipboard_manager::ClipboardExt as _; use tokio::time::Instant; // 存储应用启动时间的全局变量 diff --git a/src-tauri/src/cmd/uwp.rs b/src-tauri/src/cmd/uwp.rs index 96db7eed0..779029583 100644 --- a/src-tauri/src/cmd/uwp.rs +++ b/src-tauri/src/cmd/uwp.rs @@ -4,7 +4,7 @@ use crate::cmd::CmdResult; #[cfg(windows)] mod platform { use crate::cmd::CmdResult; - use crate::cmd::StringifyErr; + use crate::cmd::StringifyErr as _; use crate::core::win_uwp; pub fn invoke_uwp_tool() -> CmdResult { diff --git a/src-tauri/src/cmd/validate.rs b/src-tauri/src/cmd/validate.rs index 33536e3b7..a0134f340 100644 --- a/src-tauri/src/cmd/validate.rs +++ b/src-tauri/src/cmd/validate.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::{ - core::{validate::CoreConfigValidator, *}, + core::{handle, validate::CoreConfigValidator}, logging, utils::logging::Type, }; diff --git a/src-tauri/src/cmd/verge.rs b/src-tauri/src/cmd/verge.rs index c7b05fe5d..943eb9db6 100644 --- a/src-tauri/src/cmd/verge.rs +++ b/src-tauri/src/cmd/verge.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr, config::IVerge, feat, utils::draft::SharedBox}; +use crate::{cmd::StringifyErr as _, config::IVerge, feat, utils::draft::SharedBox}; /// 获取Verge配置 #[tauri::command] diff --git a/src-tauri/src/cmd/webdav.rs b/src-tauri/src/cmd/webdav.rs index 209f39e7a..30e246540 100644 --- a/src-tauri/src/cmd/webdav.rs +++ b/src-tauri/src/cmd/webdav.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::{ - cmd::StringifyErr, + cmd::StringifyErr as _, config::{Config, IVerge}, core, feat, }; @@ -19,7 +19,7 @@ pub async fn save_webdav_config(url: String, username: String, password: String) Config::verge().await.edit_draft(|e| e.patch_config(&patch)); Config::verge().await.apply(); - let verge_data = Config::verge().await.latest_arc(); + let verge_data = Config::verge().await.data_arc(); verge_data.save_file().await.stringify_err()?; core::backup::WebDavClient::global().reset(); Ok(()) diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 195ec6c93..095a0b812 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_yaml_ng::{Mapping, Value}; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, - str::FromStr, + str::FromStr as _, }; #[derive(Default, Debug, Clone)] diff --git a/src-tauri/src/config/encrypt.rs b/src-tauri/src/config/encrypt.rs index 44ac85b15..71743fae4 100644 --- a/src-tauri/src/config/encrypt.rs +++ b/src-tauri/src/config/encrypt.rs @@ -1,9 +1,9 @@ use crate::utils::dirs::get_encryption_key; use aes_gcm::{ Aes256Gcm, Key, - aead::{Aead, KeyInit}, + aead::{Aead as _, KeyInit as _}, }; -use base64::{Engine, engine::general_purpose::STANDARD}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::cell::Cell; use std::future::Future; diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index e817d7108..bdbac0183 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -6,7 +6,7 @@ use crate::{ tmpl, }, }; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 0ee3557ef..0f8a83671 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,10 +1,10 @@ use super::{PrfOption, prfitem::PrfItem}; use crate::utils::{ - dirs::{self, PathBufExec}, + dirs::{self, PathBufExec as _}, help, }; use crate::{logging, utils::logging::Type}; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::Mapping; use smartstring::alias::String; diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 3a19e451e..70ad8964d 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -158,6 +158,15 @@ pub struct IVerge { /// 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 pub auto_log_clean: Option, + /// Enable scheduled automatic backups + pub enable_auto_backup_schedule: Option, + + /// Automatic backup interval in hours + pub auto_backup_interval_hours: Option, + + /// Create backups automatically when critical configs change + pub auto_backup_on_change: Option, + /// verge 的各种 port 用于覆盖 clash 的各种 port #[cfg(not(target_os = "windows"))] pub verge_redir_port: Option, @@ -422,12 +431,15 @@ impl IVerge { auto_check_update: Some(true), enable_builtin_enhanced: Some(true), auto_log_clean: Some(2), // 1: 1天, 2: 7天, 3: 30天, 4: 90天 + enable_auto_backup_schedule: Some(false), + auto_backup_interval_hours: Some(24), + auto_backup_on_change: Some(true), webdav_url: None, webdav_username: None, webdav_password: None, enable_tray_speed: Some(false), // enable_tray_icon: Some(true), - tray_inline_proxy_groups: Some(false), + tray_inline_proxy_groups: Some(true), enable_global_hotkey: Some(true), enable_auto_light_weight_mode: Some(false), auto_light_weight_minutes: Some(10), @@ -517,6 +529,9 @@ impl IVerge { patch!(proxy_layout_column); patch!(test_list); patch!(auto_log_clean); + patch!(enable_auto_backup_schedule); + patch!(auto_backup_interval_hours); + patch!(auto_backup_on_change); patch!(webdav_url); patch!(webdav_username); diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 3195ea404..a07da7c15 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -68,10 +68,6 @@ pub mod error_patterns { } pub mod tun { - #[cfg(target_os = "linux")] - pub const DEFAULT_STACK: &str = "mixed"; - - #[cfg(not(target_os = "linux"))] pub const DEFAULT_STACK: &str = "gvisor"; pub const DNS_HIJACK: &[&str] = &["any:53"]; diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 023167580..ad3168546 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -13,7 +13,7 @@ use smartstring::alias::String; use std::{ collections::HashMap, env::{consts::OS, temp_dir}, - io::Write, + io::Write as _, path::PathBuf, sync::Arc, time::Duration, @@ -92,20 +92,20 @@ impl WebDavClient { || verge.webdav_username.is_none() || verge.webdav_password.is_none() { - let msg: String = "Unable to create web dav client, please make sure the webdav config is correct".into(); + let msg: String = + "Unable to create web dav client, please make sure the webdav config is correct".into(); return Err(anyhow::Error::msg(msg)); } let config = WebDavConfig { url: verge .webdav_url - .as_ref() - .cloned() + .clone() .unwrap_or_default() .trim_end_matches('/') .into(), - username: verge.webdav_username.as_ref().cloned().unwrap_or_default(), - password: verge.webdav_password.as_ref().cloned().unwrap_or_default(), + username: verge.webdav_username.clone().unwrap_or_default(), + password: verge.webdav_password.clone().unwrap_or_default(), }; // 存储配置到 ArcSwapOption diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index e6a711490..e03e9eeca 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tokio::sync::RwLock; use tokio::sync::{mpsc, oneshot}; use tokio::time::{Duration, sleep, timeout}; -use tokio_stream::{StreamExt, wrappers::UnboundedReceiverStream}; +use tokio_stream::{StreamExt as _, wrappers::UnboundedReceiverStream}; use crate::config::{Config, IVerge}; use crate::core::{async_proxy_query::AsyncProxyQuery, handle}; @@ -520,7 +520,7 @@ impl EventDrivenProxyManager { use crate::utils::dirs; #[allow(unused_imports)] // creation_flags必须 - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; use tokio::process::Command; let binary_path = match dirs::service_path() { diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 52d700e9d..a5c973a61 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,27 +1,33 @@ use crate::{APP_HANDLE, constants::timing, singleton}; use parking_lot::RwLock; use smartstring::alias::String; -use std::{sync::Arc, thread}; -use tauri::{AppHandle, Manager, WebviewWindow}; -use tauri_plugin_mihomo::{Mihomo, MihomoExt}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread, +}; +use tauri::{AppHandle, Manager as _, WebviewWindow}; +use tauri_plugin_mihomo::{Mihomo, MihomoExt as _}; use tokio::sync::RwLockReadGuard; use super::notification::{ErrorMessage, FrontendEvent, NotificationSystem}; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct Handle { - is_exiting: Arc>, + is_exiting: AtomicBool, startup_errors: Arc>>, - startup_completed: Arc>, + startup_completed: AtomicBool, pub(crate) notification_system: Arc>>, } impl Default for Handle { fn default() -> Self { Self { - is_exiting: Arc::new(RwLock::new(false)), + is_exiting: AtomicBool::new(false), startup_errors: Arc::new(RwLock::new(Vec::new())), - startup_completed: Arc::new(RwLock::new(false)), + startup_completed: AtomicBool::new(false), notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))), } } @@ -108,7 +114,7 @@ impl Handle { let status_str = status.into(); let msg_str = msg.into(); - if !*handle.startup_completed.read() { + if !handle.startup_completed.load(Ordering::Acquire) { handle.startup_errors.write().push(ErrorMessage { status: status_str, message: msg_str, @@ -139,7 +145,7 @@ impl Handle { } pub fn mark_startup_completed(&self) { - *self.startup_completed.write() = true; + self.startup_completed.store(true, Ordering::Release); self.send_startup_errors(); } @@ -182,7 +188,7 @@ impl Handle { } pub fn set_is_exiting(&self) { - *self.is_exiting.write() = true; + self.is_exiting.store(true, Ordering::Release); let mut system_opt = self.notification_system.write(); if let Some(system) = system_opt.as_mut() { @@ -191,7 +197,7 @@ impl Handle { } pub fn is_exiting(&self) -> bool { - *self.is_exiting.read() + self.is_exiting.load(Ordering::Acquire) } } diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index f75e7e922..44db1f019 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -8,7 +8,7 @@ use anyhow::{Result, bail}; use arc_swap::ArcSwap; use smartstring::alias::String; use std::{collections::HashMap, fmt, str::FromStr, sync::Arc}; -use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; +use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt as _, ShortcutState}; /// Enum representing all available hotkey functions #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -227,7 +227,7 @@ impl Hotkey { let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); @@ -264,7 +264,7 @@ singleton_with_logging!(Hotkey, INSTANCE, "Hotkey"); impl Hotkey { pub async fn init(&self, skip: bool) -> Result<()> { let verge = Config::verge().await; - let enable_global_hotkey = !skip && verge.latest_arc().enable_global_hotkey.unwrap_or(true); + let enable_global_hotkey = !skip && verge.data_arc().enable_global_hotkey.unwrap_or(true); logging!( debug, @@ -274,7 +274,7 @@ impl Hotkey { ); // Extract hotkeys data before async operations - let hotkeys = verge.latest_arc().hotkeys.as_ref().cloned(); + let hotkeys = verge.data_arc().hotkeys.clone(); if let Some(hotkeys) = hotkeys { logging!( diff --git a/src-tauri/src/core/manager/config.rs b/src-tauri/src/core/manager/config.rs index 295230ab0..64283a590 100644 --- a/src-tauri/src/core/manager/config.rs +++ b/src-tauri/src/core/manager/config.rs @@ -1,6 +1,6 @@ use super::CoreManager; use crate::{ - config::*, + config::{Config, ConfigType, IRuntime}, constants::timing, core::{handle, validate::CoreConfigValidator}, logging, diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index 2be3be1ba..520c1f2a1 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -16,7 +16,7 @@ use compact_str::CompactString; use flexi_logger::DeferredNow; use log::Level; use scopeguard::defer; -use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::ShellExt as _; impl CoreManager { pub async fn get_clash_logs(&self) -> Result> { diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notification.rs index eaee59322..fba5ecaa1 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notification.rs @@ -8,13 +8,13 @@ use parking_lot::RwLock; use smartstring::alias::String; use std::{ sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, mpsc, }, thread, time::{Duration, Instant}, }; -use tauri::{Emitter, WebviewWindow}; +use tauri::{Emitter as _, WebviewWindow}; #[derive(Debug, Clone)] pub enum FrontendEvent { @@ -47,7 +47,7 @@ pub struct NotificationSystem { worker_handle: Option>, pub(super) is_running: bool, stats: EventStats, - emergency_mode: RwLock, + emergency_mode: AtomicBool, } impl Default for NotificationSystem { @@ -63,7 +63,7 @@ impl NotificationSystem { worker_handle: None, is_running: false, stats: EventStats::default(), - emergency_mode: RwLock::new(false), + emergency_mode: AtomicBool::new(false), } } @@ -125,7 +125,7 @@ impl NotificationSystem { } fn should_skip_event(&self, event: &FrontendEvent) -> bool { - let is_emergency = *self.emergency_mode.read(); + let is_emergency = self.emergency_mode.load(Ordering::Acquire); matches!( (is_emergency, event), (true, FrontendEvent::NoticeMessage { status, .. }) if status == "info" @@ -184,14 +184,14 @@ impl NotificationSystem { *self.stats.last_error_time.write() = Some(Instant::now()); let errors = self.stats.total_errors.load(Ordering::Relaxed); - if errors > retry::EVENT_EMIT_THRESHOLD && !*self.emergency_mode.read() { + if errors > retry::EVENT_EMIT_THRESHOLD && !self.emergency_mode.load(Ordering::Acquire) { logging!( warn, Type::Frontend, "Entering emergency mode after {} errors", errors ); - *self.emergency_mode.write() = true; + self.emergency_mode.store(true, Ordering::Release); } } diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 1737b9666..892294d95 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -4,7 +4,7 @@ use crate::{ logging, logging_error, utils::{dirs, init::service_writer_config, logging::Type}, }; -use anyhow::{Context, Result, bail}; +use anyhow::{Context as _, Result, bail}; use clash_verge_service_ipc::CoreConfig; use compact_str::CompactString; use once_cell::sync::Lazy; @@ -37,7 +37,7 @@ async fn uninstall_service() -> Result<()> { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; let binary_path = dirs::service_path()?; let uninstall_path = binary_path.with_file_name("clash-verge-service-uninstall.exe"); @@ -72,7 +72,7 @@ async fn install_service() -> Result<()> { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; let binary_path = dirs::service_path()?; let install_path = binary_path.with_file_name("clash-verge-service-install.exe"); diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 964afa068..bd5383d97 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -12,7 +12,7 @@ use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; -use tauri_plugin_autostart::ManagerExt; +use tauri_plugin_autostart::ManagerExt as _; pub struct Sysopt { initialed: AtomicBool, @@ -59,7 +59,7 @@ async fn execute_sysproxy_command(args: Vec) -> Result<()> use crate::utils::dirs; use anyhow::bail; #[allow(unused_imports)] // Required for .creation_flags() method - use std::os::windows::process::CommandExt; + use std::os::windows::process::CommandExt as _; use tokio::process::Command; let binary_path = dirs::service_path()?; diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index c19c43dd2..20c0bb6c0 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -2,7 +2,7 @@ use crate::{ config::Config, core::sysopt::Sysopt, feat, logging, logging_error, singleton, utils::logging::Type, }; -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use parking_lot::RwLock; use smartstring::alias::String; diff --git a/src-tauri/src/core/tray/menu_def.rs b/src-tauri/src/core/tray/menu_def.rs index 78c76e6cb..24a488fdc 100644 --- a/src-tauri/src/core/tray/menu_def.rs +++ b/src-tauri/src/core/tray/menu_def.rs @@ -36,6 +36,7 @@ define_menu! { rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode", global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode", direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode", + outbound_modes => OUTBOUND_MODES, "tray_outbound_modes", "tray.outboundModes", profiles => PROFILES, "tray_profiles", "tray.profiles", proxies => PROXIES, "tray_proxies", "tray.proxies", system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy", diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 574e9294f..ce29aecc0 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -4,7 +4,7 @@ use tauri_plugin_mihomo::models::Proxies; use tokio::fs; #[cfg(target_os = "macos")] pub mod speed_rate; -use crate::config::PrfSelected; +use crate::config::{IVerge, PrfSelected}; use crate::core::service; use crate::module::lightweight; use crate::process::AsyncHandler; @@ -83,8 +83,7 @@ pub struct Tray { } impl TrayState { - pub async fn get_common_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_common_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_common_tray_icon = verge.common_tray_icon.unwrap_or(false); if is_common_tray_icon && let Ok(Some(common_icon_path)) = find_target_icons("common") @@ -120,8 +119,7 @@ impl TrayState { } } - pub async fn get_sysproxy_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_sysproxy_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_sysproxy_tray_icon = verge.sysproxy_tray_icon.unwrap_or(false); if is_sysproxy_tray_icon && let Ok(Some(sysproxy_icon_path)) = find_target_icons("sysproxy") @@ -157,8 +155,7 @@ impl TrayState { } } - pub async fn get_tun_tray_icon() -> (bool, Vec) { - let verge = Config::verge().await.latest_arc(); + async fn get_tun_tray_icon(verge: &IVerge) -> (bool, Vec) { let is_tun_tray_icon = verge.tun_tray_icon.unwrap_or(false); if is_tun_tray_icon && let Ok(Some(tun_icon_path)) = find_target_icons("tun") @@ -351,7 +348,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub async fn update_icon(&self) -> Result<()> { + pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -371,15 +368,14 @@ impl Tray { } }; - let verge = Config::verge().await.latest_arc(); 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 (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon().await, - (true, false) => TrayState::get_sysproxy_tray_icon().await, - (false, true) => TrayState::get_tun_tray_icon().await, - (false, false) => TrayState::get_common_tray_icon().await, + (true, true) => TrayState::get_tun_tray_icon(verge).await, + (true, false) => TrayState::get_sysproxy_tray_icon(verge).await, + (false, true) => TrayState::get_tun_tray_icon(verge).await, + (false, false) => TrayState::get_common_tray_icon(verge).await, }; let colorful = verge @@ -394,7 +390,7 @@ impl Tray { } #[cfg(not(target_os = "macos"))] - pub async fn update_icon(&self) -> Result<()> { + pub async fn update_icon(&self, verge: &IVerge) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘图标更新"); return Ok(()); @@ -414,15 +410,14 @@ impl Tray { } }; - let verge = Config::verge().await.latest_arc(); 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 (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { - (true, true) => TrayState::get_tun_tray_icon().await, - (true, false) => TrayState::get_sysproxy_tray_icon().await, - (false, true) => TrayState::get_tun_tray_icon().await, - (false, false) => TrayState::get_common_tray_icon().await, + (true, true) => TrayState::get_tun_tray_icon(verge).await, + (true, false) => TrayState::get_sysproxy_tray_icon(verge).await, + (false, true) => TrayState::get_tun_tray_icon(verge).await, + (false, false) => TrayState::get_common_tray_icon(verge).await, }; let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); @@ -505,13 +500,14 @@ impl Tray { logging!(debug, Type::Tray, "应用正在退出,跳过托盘局部更新"); return Ok(()); } + let verge = Config::verge().await.data_arc(); self.update_menu().await?; - self.update_icon().await?; + self.update_icon(&verge).await?; self.update_tooltip().await?; Ok(()) } - pub async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { + async fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { if handle::Handle::global().is_exiting() { logging!(debug, Type::Tray, "应用正在退出,跳过托盘创建"); return Ok(()); @@ -519,8 +515,10 @@ impl Tray { logging!(info, Type::Tray, "正在从AppHandle创建系统托盘"); + let verge = Config::verge().await.data_arc(); + // 获取图标 - let icon_bytes = TrayState::get_common_tray_icon().await.1; + let icon_bytes = TrayState::get_common_tray_icon(&verge).await.1; let icon = tauri::image::Image::from_bytes(&icon_bytes)?; #[cfg(target_os = "linux")] @@ -530,6 +528,7 @@ impl Tray { #[cfg(any(target_os = "macos", target_os = "windows"))] let show_menu_on_left_click = { + // TODO 优化这里 复用 verge let tray_event = { Config::verge().await.latest_arc().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or_else(|| "main_window".into()); tray_event.as_str() == "tray_menu" @@ -871,7 +870,7 @@ async fn create_tray_menu( }); let verge_settings = Config::verge().await.latest_arc(); - let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(false); + let show_proxy_groups_inline = verge_settings.tray_inline_proxy_groups.unwrap_or(true); let version = env!("CARGO_PKG_VERSION"); @@ -896,6 +895,13 @@ async fn create_tray_menu( hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()), )?; + let current_mode_text = match current_proxy_mode { + "global" => rust_i18n::t!("tray.global"), + "direct" => rust_i18n::t!("tray.direct"), + _ => rust_i18n::t!("tray.rule"), + }; + let outbound_modes_label = format!("{} ({})", texts.outbound_modes, current_mode_text); + let rule_mode = &CheckMenuItem::with_id( app_handle, MenuIds::RULE_MODE, @@ -923,6 +929,18 @@ async fn create_tray_menu( hotkeys.get("clash_mode_direct").map(|s| s.as_str()), )?; + let outbound_modes = &Submenu::with_id_and_items( + app_handle, + MenuIds::OUTBOUND_MODES, + outbound_modes_label.as_str(), + true, + &[ + rule_mode as &dyn IsMenuItem, + global_mode as &dyn IsMenuItem, + direct_mode as &dyn IsMenuItem, + ], + )?; + let profiles = &Submenu::with_id_and_items( app_handle, MenuIds::PROFILES, @@ -1073,6 +1091,7 @@ async fn create_tray_menu( &texts.more, true, &[ + copy_env as &dyn IsMenuItem, close_all_connections, restart_clash, restart_app, @@ -1091,15 +1110,8 @@ async fn create_tray_menu( let separator = &PredefinedMenuItem::separator(app_handle)?; // 动态构建菜单项 - let mut menu_items: Vec<&dyn IsMenuItem> = vec![ - open_window, - separator, - rule_mode, - global_mode, - direct_mode, - separator, - profiles, - ]; + let mut menu_items: Vec<&dyn IsMenuItem> = + vec![open_window, outbound_modes, separator, profiles]; // 如果有代理节点,添加代理节点菜单 if show_proxy_groups_inline { @@ -1116,7 +1128,6 @@ async fn create_tray_menu( tun_mode as &dyn IsMenuItem, separator, lightweight_mode as &dyn IsMenuItem, - copy_env as &dyn IsMenuItem, open_dir as &dyn IsMenuItem, more as &dyn IsMenuItem, separator, diff --git a/src-tauri/src/core/validate.rs b/src-tauri/src/core/validate.rs index bfb31b628..330c96f3c 100644 --- a/src-tauri/src/core/validate.rs +++ b/src-tauri/src/core/validate.rs @@ -2,7 +2,7 @@ use anyhow::Result; use scopeguard::defer; use smartstring::alias::String; use std::sync::atomic::{AtomicBool, Ordering}; -use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::ShellExt as _; use tokio::fs; use crate::config::{Config, ConfigType}; diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 7699ca133..6b74b74a0 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -5,7 +5,14 @@ mod script; pub mod seq; mod tun; -use self::{chain::*, field::*, merge::*, script::*, seq::*, tun::*}; +use self::{ + chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, + field::{use_keys, use_lowercase, use_sort}, + merge::use_merge, + script::use_script, + seq::{SeqMap, use_seq}, + tun::use_tun, +}; use crate::constants; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index a09051fac..dc78dc268 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -2,8 +2,6 @@ use serde_yaml_ng::{Mapping, Value}; #[cfg(target_os = "macos")] use crate::process::AsyncHandler; -#[cfg(target_os = "linux")] -use crate::{logging, utils::logging::Type}; macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { @@ -31,27 +29,6 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { }); if enable { - #[cfg(target_os = "linux")] - { - let stack_key = Value::from("stack"); - let should_override = match tun_val.get(&stack_key) { - Some(value) => value - .as_str() - .map(|stack| stack.eq_ignore_ascii_case("gvisor")) - .unwrap_or(false), - None => true, - }; - - if should_override { - revise!(tun_val, "stack", "mixed"); - logging!( - warn, - Type::Network, - "Warning: gVisor TUN stack detected on Linux; falling back to 'mixed' for compatibility" - ); - } - } - // 读取DNS配置 let dns_key = Value::from("dns"); let dns_val = config.get(&dns_key); diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 2aac55056..055b1d680 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -4,7 +4,7 @@ use crate::{ logging, logging_error, process::AsyncHandler, utils::{ - dirs::{PathBufExec, app_home_dir, local_backup_dir}, + dirs::{PathBufExec as _, app_home_dir, local_backup_dir}, logging::Type, }, }; @@ -123,6 +123,15 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { /// Create a backup and save to local storage pub async fn create_local_backup() -> Result<()> { + create_local_backup_with_namer(|name| name.to_string().into()) + .await + .map(|_| ()) +} + +pub async fn create_local_backup_with_namer(namer: F) -> Result +where + F: FnOnce(&str) -> String, +{ let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| { logging!( error, @@ -133,7 +142,8 @@ pub async fn create_local_backup() -> Result<()> { })?; let backup_dir = local_backup_dir()?; - let target_path = backup_dir.join(file_name.as_str()); + let final_name = namer(file_name.as_str()); + let target_path = backup_dir.join(final_name.as_str()); if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await { logging!( @@ -152,7 +162,7 @@ pub async fn create_local_backup() -> Result<()> { return Err(err); } - Ok(()) + Ok(final_name) } async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 5fe26b2b3..43e76157a 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -82,7 +82,12 @@ pub async fn change_clash_mode(mode: String) { if clash_data.save_config().await.is_ok() { handle::Handle::refresh_clash(); logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon().await); + logging_error!( + Type::Tray, + tray::Tray::global() + .update_icon(&Config::verge().await.data_arc()) + .await + ); } let is_auto_close_connection = Config::verge() diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 2bd2cfa87..cfa9aef00 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -2,7 +2,7 @@ use crate::{ config::{Config, IVerge}, core::{CoreManager, handle, hotkey, sysopt, tray}, logging_error, - module::lightweight, + module::{auto_backup::AutoBackupManager, lightweight}, utils::{draft::SharedBox, logging::Type}, }; use anyhow::Result; @@ -22,7 +22,12 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { } else { if patch.get("mode").is_some() { logging_error!(Type::Tray, tray::Tray::global().update_menu().await); - logging_error!(Type::Tray, tray::Tray::global().update_icon().await); + logging_error!( + Type::Tray, + tray::Tray::global() + .update_icon(&Config::verge().await.data_arc()) + .await + ); } Config::runtime() .await @@ -180,6 +185,7 @@ fn determine_update_flags(patch: &IVerge) -> i32 { update_flags } +#[allow(clippy::cognitive_complexity)] async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<()> { // Process updates based on flags if (update_flags & (UpdateFlags::RestartCore as i32)) != 0 { @@ -211,7 +217,9 @@ async fn process_terminated_flags(update_flags: i32, patch: &IVerge) -> Result<( tray::Tray::global().update_menu().await?; } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { - tray::Tray::global().update_icon().await?; + tray::Tray::global() + .update_icon(&Config::verge().await.latest_arc()) + .await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { tray::Tray::global().update_tooltip().await?; @@ -243,6 +251,10 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { return Err(err); } Config::verge().await.apply(); + logging_error!( + Type::Backup, + AutoBackupManager::global().refresh_settings().await + ); if !not_save_file { // 分离数据获取和异步调用 let verge_data = Config::verge().await.data_arc(); diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index ff4e42928..83446aeb9 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -7,7 +7,7 @@ use crate::{ }; use anyhow::{Result, bail}; use smartstring::alias::String; -use tauri::Emitter; +use tauri::Emitter as _; /// Toggle proxy profile pub async fn toggle_proxy_profile(profile_index: String) { diff --git a/src-tauri/src/feat/proxy.rs b/src-tauri/src/feat/proxy.rs index 9f7d513e8..69d4e0339 100644 --- a/src-tauri/src/feat/proxy.rs +++ b/src-tauri/src/feat/proxy.rs @@ -5,7 +5,7 @@ use crate::{ utils::logging::Type, }; use std::env; -use tauri_plugin_clipboard_manager::ClipboardExt; +use tauri_plugin_clipboard_manager::ClipboardExt as _; /// Toggle system proxy on/off pub async fn toggle_system_proxy() { diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 8352fea1b..876ba4432 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -47,7 +47,7 @@ pub async fn clean_async() -> bool { let tun_task = async { let tun_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_tun_mode .unwrap_or(false); @@ -57,13 +57,8 @@ pub async fn clean_async() -> bool { let disable_tun = serde_json::json!({ "tun": { "enable": false } }); - #[cfg(target_os = "windows")] - let tun_timeout = Duration::from_millis(100); - #[cfg(not(target_os = "windows"))] - let tun_timeout = Duration::from_millis(100); - match timeout( - tun_timeout, + Duration::from_millis(1000), handle::Handle::mihomo() .await .patch_base_config(&disable_tun), @@ -100,7 +95,7 @@ pub async fn clean_async() -> bool { // 检查系统代理是否开启 let sys_proxy_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_system_proxy .unwrap_or(false); @@ -176,7 +171,7 @@ pub async fn clean_async() -> bool { { let sys_proxy_enabled = Config::verge() .await - .latest_arc() + .data_arc() .enable_system_proxy .unwrap_or(false); @@ -316,7 +311,7 @@ pub async fn hide() { let enable_auto_light_weight_mode = Config::verge() .await - .latest_arc() + .data_arc() .enable_auto_light_weight_mode .unwrap_or(false); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 82b1e4875..3a4808f7b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,25 +11,20 @@ mod module; mod process; pub mod utils; use crate::constants::files; -#[cfg(target_os = "macos")] -use crate::module::lightweight; #[cfg(target_os = "linux")] use crate::utils::linux; -#[cfg(target_os = "macos")] -use crate::utils::window_manager::WindowManager; use crate::{ - core::{EventDrivenProxyManager, handle, hotkey}, + core::{EventDrivenProxyManager, handle}, process::AsyncHandler, utils::resolve, }; use anyhow::Result; -use config::Config; use once_cell::sync::OnceCell; use rust_i18n::i18n; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager as _}; #[cfg(target_os = "macos")] use tauri_plugin_autostart::MacosLauncher; -use tauri_plugin_deep_link::DeepLinkExt; +use tauri_plugin_deep_link::DeepLinkExt as _; use utils::logging::Type; i18n!("locales", fallback = "zh"); @@ -267,8 +262,20 @@ pub fn run() { .invoke_handler(app_init::generate_handlers()); mod event_handlers { - use super::*; - use crate::core::handle; + #[cfg(target_os = "macos")] + use crate::module::lightweight; + #[cfg(target_os = "macos")] + use crate::utils::window_manager::WindowManager; + use crate::{ + config::Config, + core::{self, handle, hotkey}, + logging, + process::AsyncHandler, + utils::logging::Type, + }; + use tauri::AppHandle; + #[cfg(target_os = "macos")] + use tauri::Manager as _; pub fn handle_ready_resumed(_app_handle: &AppHandle) { if handle::Handle::global().is_exiting() { @@ -320,7 +327,7 @@ pub fn run() { AsyncHandler::spawn(move || async move { let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); @@ -360,7 +367,7 @@ pub fn run() { let _ = hotkey::Hotkey::global().unregister_system_hotkey(SystemHotkey::CmdW); let is_enable_global_hotkey = Config::verge() .await - .latest_arc() + .data_arc() .enable_global_hotkey .unwrap_or(true); if !is_enable_global_hotkey { @@ -416,7 +423,7 @@ pub fn run() { }); } tauri::RunEvent::ExitRequested { api, code, .. } => { - tauri::async_runtime::block_on(async { + AsyncHandler::block_on(async { let _ = handle::Handle::mihomo() .await .clear_all_ws_connections() diff --git a/src-tauri/src/module/auto_backup.rs b/src-tauri/src/module/auto_backup.rs new file mode 100644 index 000000000..2cb9c89bb --- /dev/null +++ b/src-tauri/src/module/auto_backup.rs @@ -0,0 +1,332 @@ +use crate::{ + config::{Config, IVerge}, + feat::create_local_backup_with_namer, + logging, + process::AsyncHandler, + utils::{dirs::local_backup_dir, logging::Type}, +}; +use anyhow::Result; +use chrono::Local; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use std::{ + path::PathBuf, + sync::{ + Arc, + atomic::{AtomicBool, AtomicI64, Ordering}, + }, + time::{Duration, UNIX_EPOCH}, +}; +use tokio::{ + fs, + sync::{Mutex, watch}, +}; + +const DEFAULT_INTERVAL_HOURS: u64 = 24; +const MIN_INTERVAL_HOURS: u64 = 1; +const MAX_INTERVAL_HOURS: u64 = 168; +const MIN_BACKUP_INTERVAL_SECS: i64 = 60; +const AUTO_BACKUP_KEEP: usize = 20; +const AUTO_MARKER: &str = "-auto-"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AutoBackupTrigger { + Scheduled, + GlobalMerge, + GlobalScript, + ProfileChange, +} + +impl AutoBackupTrigger { + const fn slug(self) -> &'static str { + match self { + Self::Scheduled => "scheduled", + Self::GlobalMerge => "merge", + Self::GlobalScript => "script", + Self::ProfileChange => "profile", + } + } + + const fn is_schedule(self) -> bool { + matches!(self, Self::Scheduled) + } +} + +#[derive(Clone, Copy, Debug)] +struct AutoBackupSettings { + schedule_enabled: bool, + interval_hours: u64, + change_enabled: bool, +} + +impl AutoBackupSettings { + fn from_verge(verge: &IVerge) -> Self { + let interval = verge + .auto_backup_interval_hours + .unwrap_or(DEFAULT_INTERVAL_HOURS) + .clamp(MIN_INTERVAL_HOURS, MAX_INTERVAL_HOURS); + + Self { + schedule_enabled: verge.enable_auto_backup_schedule.unwrap_or(false), + interval_hours: interval, + change_enabled: verge.auto_backup_on_change.unwrap_or(true), + } + } +} + +impl Default for AutoBackupSettings { + fn default() -> Self { + Self { + schedule_enabled: false, + interval_hours: DEFAULT_INTERVAL_HOURS, + change_enabled: true, + } + } +} + +pub struct AutoBackupManager { + settings: Arc>, + settings_tx: watch::Sender, + runner_started: AtomicBool, + exec_lock: Mutex<()>, + last_backup: AtomicI64, +} + +impl AutoBackupManager { + pub fn global() -> &'static Self { + static INSTANCE: OnceCell = OnceCell::new(); + INSTANCE.get_or_init(|| { + let (tx, _rx) = watch::channel(AutoBackupSettings::default()); + Self { + settings: Arc::new(RwLock::new(AutoBackupSettings::default())), + settings_tx: tx, + runner_started: AtomicBool::new(false), + exec_lock: Mutex::new(()), + last_backup: AtomicI64::new(0), + } + }) + } + + pub async fn init(&self) -> Result<()> { + let settings = Self::load_settings().await; + { + *self.settings.write() = settings; + } + let _ = self.settings_tx.send(settings); + self.maybe_start_runner(settings); + Ok(()) + } + + pub async fn refresh_settings(&self) -> Result<()> { + let settings = Self::load_settings().await; + { + *self.settings.write() = settings; + } + let _ = self.settings_tx.send(settings); + self.maybe_start_runner(settings); + Ok(()) + } + + pub fn trigger_backup(trigger: AutoBackupTrigger) { + AsyncHandler::spawn(move || async move { + if let Err(err) = Self::global().execute_trigger(trigger).await { + logging!( + warn, + Type::Backup, + "Auto backup execution failed ({:?}): {err:#?}", + trigger + ); + } + }); + } + + fn maybe_start_runner(&self, settings: AutoBackupSettings) { + if settings.schedule_enabled { + self.ensure_runner(); + } + } + + fn ensure_runner(&self) { + if self.runner_started.swap(true, Ordering::SeqCst) { + return; + } + + let mut rx = self.settings_tx.subscribe(); + AsyncHandler::spawn(move || async move { + Self::run_scheduler(&mut rx).await; + }); + } + + async fn run_scheduler(rx: &mut watch::Receiver) { + let mut current = *rx.borrow(); + loop { + if !current.schedule_enabled { + if rx.changed().await.is_err() { + break; + } + current = *rx.borrow(); + continue; + } + + let duration = Duration::from_secs(current.interval_hours.saturating_mul(3600)); + let sleeper = tokio::time::sleep(duration); + tokio::pin!(sleeper); + + tokio::select! { + _ = &mut sleeper => { + if let Err(err) = Self::global() + .execute_trigger(AutoBackupTrigger::Scheduled) + .await + { + logging!( + warn, + Type::Backup, + "Scheduled auto backup failed: {err:#?}" + ); + } + } + changed = rx.changed() => { + if changed.is_err() { + break; + } + current = *rx.borrow(); + } + } + } + } + + async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { + let snapshot = *self.settings.read(); + + if trigger.is_schedule() && !snapshot.schedule_enabled { + return Ok(()); + } + if !trigger.is_schedule() && !snapshot.change_enabled { + return Ok(()); + } + + if !self.should_run_now() { + return Ok(()); + } + + let _guard = self.exec_lock.lock().await; + if !self.should_run_now() { + return Ok(()); + } + + let file_name = + create_local_backup_with_namer(|name| append_auto_suffix(name, trigger.slug()).into()) + .await?; + self.last_backup + .store(Local::now().timestamp(), Ordering::Release); + + if let Err(err) = cleanup_auto_backups().await { + logging!( + warn, + Type::Backup, + "Failed to cleanup old auto backups: {err:#?}" + ); + } + + logging!( + info, + Type::Backup, + "Auto backup created ({:?}): {}", + trigger, + file_name + ); + Ok(()) + } + + fn should_run_now(&self) -> bool { + let last = self.last_backup.load(Ordering::Acquire); + if last == 0 { + return true; + } + let now = Local::now().timestamp(); + now.saturating_sub(last) >= MIN_BACKUP_INTERVAL_SECS + } + + async fn load_settings() -> AutoBackupSettings { + let verge = Config::verge().await; + AutoBackupSettings::from_verge(&verge.latest_arc()) + } +} + +fn append_auto_suffix(file_name: &str, slug: &str) -> String { + match file_name.rsplit_once('.') { + Some((stem, ext)) => format!("{stem}{AUTO_MARKER}{slug}.{ext}"), + None => format!("{file_name}{AUTO_MARKER}{slug}"), + } +} + +async fn cleanup_auto_backups() -> Result<()> { + if AUTO_BACKUP_KEEP == 0 { + return Ok(()); + } + + let backup_dir = local_backup_dir()?; + if !backup_dir.exists() { + return Ok(()); + } + + let mut entries = match fs::read_dir(&backup_dir).await { + Ok(dir) => dir, + Err(err) => { + logging!( + warn, + Type::Backup, + "Failed to read backup directory: {err:#?}" + ); + return Ok(()); + } + }; + + let mut files: Vec<(PathBuf, u64)> = Vec::new(); + + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let file_name = match entry.file_name().into_string() { + Ok(name) => name, + Err(_) => continue, + }; + + if !file_name.contains(AUTO_MARKER) { + continue; + } + + let modified = entry + .metadata() + .await + .and_then(|meta| meta.modified()) + .ok() + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|dur| dur.as_secs()) + .unwrap_or(0); + + files.push((path, modified)); + } + + if files.len() <= AUTO_BACKUP_KEEP { + return Ok(()); + } + + files.sort_by_key(|(_, ts)| *ts); + let remove_count = files.len() - AUTO_BACKUP_KEEP; + for (path, _) in files.into_iter().take(remove_count) { + if let Err(err) = fs::remove_file(&path).await { + logging!( + warn, + Type::Backup, + "Failed to remove auto backup {}: {err:#?}", + path.display() + ); + } + } + + Ok(()) +} diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index 30dd141cb..25978b163 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -10,10 +10,10 @@ use crate::{ use crate::logging_error; use crate::utils::window_manager::WindowManager; -use anyhow::{Context, Result}; +use anyhow::{Context as _, Result}; use delay_timer::prelude::TaskBuilder; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering}; -use tauri::Listener; +use tauri::Listener as _; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; diff --git a/src-tauri/src/module/mod.rs b/src-tauri/src/module/mod.rs index 372ad5ba0..87055c6ca 100644 --- a/src-tauri/src/module/mod.rs +++ b/src-tauri/src/module/mod.rs @@ -1,3 +1,4 @@ +pub mod auto_backup; pub mod lightweight; pub mod signal; pub mod sysinfo; diff --git a/src-tauri/src/module/signal/windows.rs b/src-tauri/src/module/signal/windows.rs index f9c13b67b..f10b415cc 100644 --- a/src-tauri/src/module/signal/windows.rs +++ b/src-tauri/src/module/signal/windows.rs @@ -1,4 +1,4 @@ -use tauri::Manager; +use tauri::Manager as _; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, UI::WindowsAndMessaging::{ diff --git a/src-tauri/src/process/async_handler.rs b/src-tauri/src/process/async_handler.rs index eeb60cf57..8c7966207 100644 --- a/src-tauri/src/process/async_handler.rs +++ b/src-tauri/src/process/async_handler.rs @@ -12,6 +12,7 @@ impl AsyncHandler { // async_runtime::handle() // } + #[inline] #[track_caller] pub fn spawn(f: F) -> JoinHandle<()> where @@ -23,6 +24,7 @@ impl AsyncHandler { async_runtime::spawn(f()) } + #[inline] #[track_caller] pub fn spawn_blocking(f: F) -> JoinHandle where @@ -34,7 +36,7 @@ impl AsyncHandler { async_runtime::spawn_blocking(f) } - #[allow(dead_code)] + #[inline] #[track_caller] pub fn block_on(fut: Fut) -> Fut::Output where diff --git a/src-tauri/src/process/guard.rs b/src-tauri/src/process/guard.rs index 0ffb89990..d1a41ab50 100644 --- a/src-tauri/src/process/guard.rs +++ b/src-tauri/src/process/guard.rs @@ -7,6 +7,7 @@ use crate::{logging, utils::logging::Type}; pub struct CommandChildGuard(Option); impl Drop for CommandChildGuard { + #[inline] fn drop(&mut self) { if let Err(err) = self.kill() { logging!( @@ -20,10 +21,12 @@ impl Drop for CommandChildGuard { } impl CommandChildGuard { + #[inline] pub const fn new(child: CommandChild) -> Self { Self(Some(child)) } + #[inline] pub fn kill(&mut self) -> Result<()> { if let Some(child) = self.0.take() { let _ = child.kill(); @@ -31,6 +34,7 @@ impl CommandChildGuard { Ok(()) } + #[inline] pub fn pid(&self) -> Option { self.0.as_ref().map(|c| c.pid()) } diff --git a/src-tauri/src/utils/autostart.rs b/src-tauri/src/utils/autostart.rs index b4dc4965a..d898a6d09 100644 --- a/src-tauri/src/utils/autostart.rs +++ b/src-tauri/src/utils/autostart.rs @@ -4,7 +4,7 @@ use crate::{logging, utils::logging::Type}; use anyhow::{Result, anyhow}; #[cfg(target_os = "windows")] -use std::{os::windows::process::CommandExt, path::Path, path::PathBuf}; +use std::{os::windows::process::CommandExt as _, path::Path, path::PathBuf}; /// Windows 下的开机启动文件夹路径 #[cfg(target_os = "windows")] @@ -37,7 +37,7 @@ pub fn get_exe_path() -> Result { /// 创建快捷方式 #[cfg(target_os = "windows")] pub async fn create_shortcut() -> Result<()> { - use crate::utils::dirs::PathBufExec; + use crate::utils::dirs::PathBufExec as _; let exe_path = get_exe_path()?; let startup_dir = get_startup_dir()?; @@ -90,7 +90,7 @@ pub async fn create_shortcut() -> Result<()> { /// 删除快捷方式 #[cfg(target_os = "windows")] pub async fn remove_shortcut() -> Result<()> { - use crate::utils::dirs::PathBufExec; + use crate::utils::dirs::PathBufExec as _; let startup_dir = get_startup_dir()?; let old_shortcut_path = startup_dir.join("Clash-Verge.lnk"); diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 56c74f2bc..f00e33f33 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -9,7 +9,7 @@ use once_cell::sync::OnceCell; #[cfg(unix)] use std::iter; use std::{fs, path::PathBuf}; -use tauri::Manager; +use tauri::Manager as _; #[cfg(not(feature = "verge-dev"))] pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev"; @@ -103,31 +103,25 @@ pub fn app_icons_dir() -> Result { pub fn find_target_icons(target: &str) -> Result> { let icons_dir = app_icons_dir()?; - let mut matching_files = Vec::new(); + let icon_path = fs::read_dir(&icons_dir)? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .find(|path| { + let prefix_matches = path + .file_prefix() + .and_then(|p| p.to_str()) + .is_some_and(|prefix| prefix.starts_with(target)); + let ext_matches = path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|ext| { + ext.eq_ignore_ascii_case("ico") || ext.eq_ignore_ascii_case("png") + }); + prefix_matches && ext_matches + }); - 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()) - && 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 { - match matching_files.first() { - Some(first_path) => { - let first = path_to_str(first_path)?; - Ok(Some(first.into())) - } - None => Ok(None), - } - } + icon_path + .map(|path| path_to_str(&path).map(|s| s.into())) + .transpose() } /// logs dir diff --git a/src-tauri/src/utils/draft.rs b/src-tauri/src/utils/draft.rs index cc81b1f2d..07782f4bd 100644 --- a/src-tauri/src/utils/draft.rs +++ b/src-tauri/src/utils/draft.rs @@ -12,12 +12,14 @@ pub struct Draft { } impl Draft { + #[inline] pub fn new(data: T) -> Self { Self { inner: Arc::new(RwLock::new((Arc::new(Box::new(data)), None))), } } /// 以 Arc> 的形式获取当前“已提交(正式)”数据的快照(零拷贝,仅 clone Arc) + #[inline] pub fn data_arc(&self) -> SharedBox { let guard = self.inner.read(); Arc::clone(&guard.0) @@ -25,18 +27,16 @@ impl Draft { /// 获取当前(草稿若存在则返回草稿,否则返回已提交)的快照 /// 这也是零拷贝:只 clone Arc,不 clone T + #[inline] pub fn latest_arc(&self) -> SharedBox { let guard = self.inner.read(); - guard - .1 - .as_ref() - .cloned() - .unwrap_or_else(|| Arc::clone(&guard.0)) + guard.1.clone().unwrap_or_else(|| Arc::clone(&guard.0)) } /// 通过闭包以可变方式编辑草稿(在闭包中我们给出 &mut T) /// - 延迟拷贝:如果只有这一个 Arc 引用,则直接修改,不会克隆 T; /// - 若草稿被其他读者共享,Arc::make_mut 会做一次 T.clone(最小必要拷贝)。 + #[inline] pub fn edit_draft(&self, f: F) -> R where F: FnOnce(&mut T) -> R, @@ -60,6 +60,7 @@ impl Draft { } /// 将草稿提交到已提交位置(替换),并清除草稿 + #[inline] pub fn apply(&self) { let mut guard = self.inner.write(); if let Some(d) = guard.1.take() { @@ -68,6 +69,7 @@ impl Draft { } /// 丢弃草稿(如果存在) + #[inline] pub fn discard(&self) { let mut guard = self.inner.write(); guard.1 = None; @@ -75,6 +77,7 @@ impl Draft { /// 异步地以拥有 Box 的方式修改已提交数据:将克隆一次已提交数据到本地, /// 异步闭包返回新的 Box(替换已提交数据)和业务返回值 R。 + #[inline] pub async fn with_data_modify(&self, f: F) -> Result where T: Send + Sync + 'static, diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 1fc58d036..a33b365a4 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,5 +1,5 @@ use crate::{config::with_encryption, enhance::seq::SeqMap, logging, utils::logging::Type}; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context as _, Result, anyhow, bail}; use nanoid::nanoid; use serde::{Serialize, de::DeserializeOwned}; use serde_yaml_ng::Mapping; diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index a2f7de0a6..c7ab73095 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -2,26 +2,26 @@ #[cfg(not(feature = "tauri-dev"))] use crate::utils::logging::NoModuleFilter; use crate::{ - config::*, + config::{Config, IClashTemp, IProfiles, IVerge}, constants, core::handle, logging, process::AsyncHandler, utils::{ - dirs::{self, PathBufExec, service_log_dir, sidecar_log_dir}, + dirs::{self, PathBufExec as _, service_log_dir, sidecar_log_dir}, help, logging::Type, }, }; use anyhow::Result; -use chrono::{Local, TimeZone}; +use chrono::{Local, TimeZone as _}; use clash_verge_service_ipc::WriterConfig; use flexi_logger::writers::FileLogWriter; use flexi_logger::{Cleanup, Criterion, FileSpec}; #[cfg(not(feature = "tauri-dev"))] use flexi_logger::{Duplicate, LogSpecBuilder, Logger}; -use std::{path::PathBuf, str::FromStr}; -use tauri_plugin_shell::ShellExt; +use std::{path::PathBuf, str::FromStr as _}; +use tauri_plugin_shell::ShellExt as _; use tokio::fs; use tokio::fs::DirEntry; @@ -31,7 +31,7 @@ pub async fn init_logger() -> Result<()> { // TODO 提供 runtime 级别实时修改 let (log_level, log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.get_log_level(), verge.app_log_max_size.unwrap_or(128), @@ -89,7 +89,7 @@ pub async fn init_logger() -> Result<()> { pub async fn sidecar_writer() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -117,7 +117,7 @@ pub async fn sidecar_writer() -> Result { pub async fn service_writer_config() -> Result { let (log_max_size, log_max_count) = { let verge_guard = Config::verge().await; - let verge = verge_guard.latest_arc(); + let verge = verge_guard.data_arc(); ( verge.app_log_max_size.unwrap_or(128), verge.app_log_max_count.unwrap_or(8), @@ -142,7 +142,7 @@ pub async fn delete_log() -> Result<()> { let auto_log_clean = { let verge = Config::verge().await; - let verge = verge.latest_arc(); + let verge = verge.data_arc(); verge.auto_log_clean.unwrap_or(0) }; @@ -466,7 +466,7 @@ pub async fn startup_script() -> Result<()> { let app_handle = handle::Handle::app_handle(); let script_path = { let verge = Config::verge().await; - let verge = verge.latest_arc(); + let verge = verge.data_arc(); verge.startup_script.clone().unwrap_or_else(|| "".into()) }; diff --git a/src-tauri/src/utils/linux.rs b/src-tauri/src/utils/linux.rs index 65e0ae49f..e3ced3871 100644 --- a/src-tauri/src/utils/linux.rs +++ b/src-tauri/src/utils/linux.rs @@ -262,7 +262,9 @@ impl DmabufDecision { if intel_gpu.inconclusive { decision.message = Some("Wayland 上检测到 Intel GPU,但缺少 boot_vga 信息:预防性禁用 WebKit DMABUF,若确认非主 GPU 可通过 CLASH_VERGE_DMABUF=1 覆盖。".into()); } else { - decision.message = Some("Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into()); + decision.message = Some( + "Wayland 上检测到 Intel 主 GPU (0x8086):禁用 WebKit DMABUF 以避免帧缓冲失败。".into(), + ); } } else if session.is_wayland { decision.message = Some( diff --git a/src-tauri/src/utils/logging.rs b/src-tauri/src/utils/logging.rs index 35353c81e..7b5de419b 100644 --- a/src-tauri/src/utils/logging.rs +++ b/src-tauri/src/utils/logging.rs @@ -3,7 +3,7 @@ use flexi_logger::DeferredNow; #[cfg(not(feature = "tauri-dev"))] use flexi_logger::filter::LogLineFilter; use flexi_logger::writers::FileLogWriter; -use flexi_logger::writers::LogWriter; +use flexi_logger::writers::LogWriter as _; use log::Level; use log::Record; use std::{fmt, sync::Arc}; diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index c4598a7b9..400bf21c3 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -159,10 +159,10 @@ impl NetworkManager { ProxyType::None => None, ProxyType::Localhost => { let port = { - let verge_port = Config::verge().await.latest_arc().verge_mixed_port; + let verge_port = Config::verge().await.data_arc().verge_mixed_port; match verge_port { Some(port) => port, - None => Config::clash().await.latest_arc().get_mixed_port(), + None => Config::clash().await.data_arc().get_mixed_port(), } }; let proxy_scheme = format!("http://127.0.0.1:{port}"); diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/utils/notification.rs index 5370132d3..d7ff371a5 100644 --- a/src-tauri/src/utils/notification.rs +++ b/src-tauri/src/utils/notification.rs @@ -1,5 +1,5 @@ use crate::{core::handle, utils::i18n}; -use tauri_plugin_notification::NotificationExt; +use tauri_plugin_notification::NotificationExt as _; pub enum NotificationEvent<'a> { DashboardToggled, diff --git a/src-tauri/src/utils/resolve/dns.rs b/src-tauri/src/utils/resolve/dns.rs index e225f0c80..0896bccaf 100644 --- a/src-tauri/src/utils/resolve/dns.rs +++ b/src-tauri/src/utils/resolve/dns.rs @@ -3,7 +3,7 @@ use crate::{logging, utils::logging::Type}; pub async fn set_public_dns(dns_server: String) { use crate::utils::logging::Type; use crate::{core::handle, logging, utils::dirs}; - use tauri_plugin_shell::ShellExt; + use tauri_plugin_shell::ShellExt as _; let app_handle = handle::Handle::app_handle(); logging!(info, Type::Config, "try to set system dns"); @@ -50,7 +50,7 @@ pub async fn set_public_dns(dns_server: String) { #[cfg(target_os = "macos")] pub async fn restore_public_dns() { use crate::{core::handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; + use tauri_plugin_shell::ShellExt as _; let app_handle = handle::Handle::app_handle(); logging!(info, Type::Config, "try to unset system dns"); let resource_dir = match dirs::app_resources_dir() { diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 83a1d7045..4ea2d376f 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -10,7 +10,7 @@ use crate::{ tray::Tray, }, logging, logging_error, - module::{lightweight::auto_lightweight_boot, signal}, + module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot, signal}, process::AsyncHandler, utils::{init, logging::Type, server, window_manager::WindowManager}, }; @@ -67,6 +67,7 @@ pub fn resolve_setup_async() { init_timer(), init_hotkey(), init_auto_lightweight_boot(), + init_auto_backup(), ); }); } @@ -122,6 +123,10 @@ pub(super) async fn init_auto_lightweight_boot() { logging_error!(Type::Setup, auto_lightweight_boot().await); } +pub(super) async fn init_auto_backup() { + logging_error!(Type::Setup, AutoBackupManager::global().init().await); +} + pub(super) fn init_signal() { logging!(info, Type::Setup, "Initializing signal handlers..."); signal::register(); @@ -174,7 +179,7 @@ pub(super) async fn refresh_tray_menu() { pub(super) async fn init_window() { let is_silent_start = Config::verge() .await - .latest_arc() + .data_arc() .enable_silent_start .unwrap_or(false); #[cfg(target_os = "macos")] diff --git a/src-tauri/src/utils/resolve/ui.rs b/src-tauri/src/utils/resolve/ui.rs index 55d5d8528..d93fe9e6f 100644 --- a/src-tauri/src/utils/resolve/ui.rs +++ b/src-tauri/src/utils/resolve/ui.rs @@ -1,50 +1,37 @@ use once_cell::sync::OnceCell; -use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; use std::sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; use tokio::sync::Notify; use crate::{logging, utils::logging::Type}; -// 使用 AtomicBool 替代 RwLock,性能更好且无锁 -static UI_READY: OnceCell = OnceCell::new(); +// 获取 UI 是否准备就绪的全局状态 +static UI_READY: AtomicBool = AtomicBool::new(false); // 获取UI就绪状态细节 -static UI_READY_STATE: OnceCell = OnceCell::new(); +static UI_READY_STATE: AtomicU8 = AtomicU8::new(0); // 添加通知机制,用于事件驱动的 UI 就绪检测 static UI_READY_NOTIFY: OnceCell> = OnceCell::new(); // UI就绪阶段状态枚举 -#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum UiReadyStage { - NotStarted, + NotStarted = 0, Loading, DomReady, ResourcesLoaded, Ready, } -// UI就绪详细状态 -#[derive(Debug)] -struct UiReadyState { - stage: RwLock, +pub fn get_ui_ready() -> &'static AtomicBool { + &UI_READY } -impl Default for UiReadyState { - fn default() -> Self { - Self { - stage: RwLock::new(UiReadyStage::NotStarted), - } - } -} - -pub(super) fn get_ui_ready() -> &'static AtomicBool { - UI_READY.get_or_init(|| AtomicBool::new(false)) -} - -fn get_ui_ready_state() -> &'static UiReadyState { - UI_READY_STATE.get_or_init(UiReadyState::default) +fn get_ui_ready_state() -> &'static AtomicU8 { + &UI_READY_STATE } fn get_ui_ready_notify() -> &'static Arc { @@ -53,8 +40,7 @@ fn get_ui_ready_notify() -> &'static Arc { // 更新UI准备阶段 pub fn update_ui_ready_stage(stage: UiReadyStage) { - let state = get_ui_ready_state(); - *state.stage.write() = stage; + get_ui_ready_state().store(stage as u8, Ordering::Release); // 如果是最终阶段,标记UI完全就绪 if stage == UiReadyStage::Ready { mark_ui_ready(); diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index dda3c4874..d50af28dd 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -7,7 +7,7 @@ use crate::{ use once_cell::sync::OnceCell; use parking_lot::Mutex; use tokio::sync::oneshot; -use warp::Filter; +use warp::Filter as _; // 关闭 embedded server 的信号发送端 static SHUTDOWN_SENDER: OnceCell>>> = OnceCell::new(); @@ -26,15 +26,15 @@ pub fn embed_server() { let clash_config = Config::clash().await; let pac_content = verge_config - .latest_arc() + .data_arc() .pac_file_content .clone() .unwrap_or_else(|| DEFAULT_PAC.into()); let pac_port = verge_config - .latest_arc() + .data_arc() .verge_mixed_port - .unwrap_or_else(|| clash_config.latest_arc().get_mixed_port()); + .unwrap_or_else(|| clash_config.data_arc().get_mixed_port()); let pac = warp::path!("commands" / "pac").map(move || { let processed_content = pac_content.replace("%mixed-port%", &format!("{pac_port}")); diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index 801b38942..c6a535696 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -5,7 +5,7 @@ use crate::{ }; use std::future::Future; use std::pin::Pin; -use tauri::{Manager, WebviewWindow, Wry}; +use tauri::{Manager as _, WebviewWindow, Wry}; use once_cell::sync::OnceCell; use parking_lot::Mutex; diff --git a/src/components/connection/connection-column-manager.tsx b/src/components/connection/connection-column-manager.tsx new file mode 100644 index 000000000..f2a5a6e42 --- /dev/null +++ b/src/components/connection/connection-column-manager.tsx @@ -0,0 +1,191 @@ +import { + closestCenter, + DndContext, + PointerSensor, + useSensor, + useSensors, + type DragEndEvent, +} from "@dnd-kit/core"; +import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { DragIndicatorRounded } from "@mui/icons-material"; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + List, + ListItem, + ListItemText, +} from "@mui/material"; +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +interface ColumnOption { + field: string; + label: string; + visible: boolean; +} + +interface Props { + open: boolean; + columns: ColumnOption[]; + onClose: () => void; + onToggle: (field: string, visible: boolean) => void; + onOrderChange: (order: string[]) => void; + onReset: () => void; +} + +export const ConnectionColumnManager = ({ + open, + columns, + onClose, + onToggle, + onOrderChange, + onReset, +}: Props) => { + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 6 }, + }), + ); + const { t } = useTranslation(); + + const items = useMemo(() => columns.map((column) => column.field), [columns]); + const visibleCount = useMemo( + () => columns.filter((column) => column.visible).length, + [columns], + ); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const order = columns.map((column) => column.field); + const oldIndex = order.indexOf(active.id as string); + const newIndex = order.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return; + + onOrderChange(arrayMove(order, oldIndex, newIndex)); + }, + [columns, onOrderChange], + ); + + return ( + + + {t("connections.components.columnManager.title")} + + + + + + {columns.map((column) => ( + + ))} + + + + + + + + + + ); +}; + +interface SortableColumnItemProps { + column: ColumnOption; + onToggle: (field: string, visible: boolean) => void; + dragHandleLabel: string; + disableToggle?: boolean; +} + +const SortableColumnItem = ({ + column, + onToggle, + dragHandleLabel, + disableToggle = false, +}: SortableColumnItemProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: column.field }); + + const style = useMemo( + () => ({ + transform: CSS.Transform.toString(transform), + transition, + }), + [transform, transition], + ); + + return ( + `1px solid ${theme.palette.divider}`, + backgroundColor: isDragging ? "action.hover" : "transparent", + display: "flex", + alignItems: "center", + gap: 1, + }} + style={style} + > + onToggle(column.field, event.target.checked)} + /> + + + + + + ); +}; diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index 8761a522a..f1eebb535 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -1,24 +1,63 @@ +import { Box } from "@mui/material"; import { DataGrid, GridColDef, + GridColumnOrderChangeParams, GridColumnResizeParams, + GridColumnVisibilityModel, useGridApiRef, + GridColumnMenuItemProps, + GridColumnMenuHideItem, + useGridRootProps, } from "@mui/x-data-grid"; import dayjs from "dayjs"; import { useLocalStorage } from "foxact/use-local-storage"; -import { useLayoutEffect, useMemo, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + createContext, + use, +} from "react"; +import type { MouseEvent } from "react"; import { useTranslation } from "react-i18next"; import parseTraffic from "@/utils/parse-traffic"; import { truncateStr } from "@/utils/truncate-str"; +import { ConnectionColumnManager } from "./connection-column-manager"; + +const ColumnManagerContext = createContext<() => void>(() => {}); + +/** + * Reconcile stored column order with base columns to handle added/removed fields + */ +const reconcileColumnOrder = ( + storedOrder: string[], + baseFields: string[], +): string[] => { + const filtered = storedOrder.filter((field) => baseFields.includes(field)); + const missing = baseFields.filter((field) => !filtered.includes(field)); + return [...filtered, ...missing]; +}; + interface Props { connections: IConnectionsItem[]; onShowDetail: (data: IConnectionsItem) => void; + columnManagerOpen: boolean; + onOpenColumnManager: () => void; + onCloseColumnManager: () => void; } export const ConnectionTable = (props: Props) => { - const { connections, onShowDetail } = props; + const { + connections, + onShowDetail, + columnManagerOpen, + onOpenColumnManager, + onCloseColumnManager, + } = props; const { t } = useTranslation(); const apiRef = useGridApiRef(); useLayoutEffect(() => { @@ -145,10 +184,6 @@ export const ConnectionTable = (props: Props) => { }; }, [apiRef]); - const [columnVisible, setColumnVisible] = useState< - Partial> - >({}); - const [columnWidths, setColumnWidths] = useLocalStorage< Record >( @@ -158,7 +193,43 @@ export const ConnectionTable = (props: Props) => { {}, ); - const columns = useMemo(() => { + const [columnVisibilityModel, setColumnVisibilityModel] = useLocalStorage< + Partial> + >( + "connection-table-visibility", + {}, + { + serializer: JSON.stringify, + deserializer: (value) => { + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === "object") return parsed; + } catch (err) { + console.warn("Failed to parse connection-table-visibility", err); + } + return {}; + }, + }, + ); + + const [columnOrder, setColumnOrder] = useLocalStorage( + "connection-table-order", + [], + { + serializer: JSON.stringify, + deserializer: (value) => { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) return parsed; + } catch (err) { + console.warn("Failed to parse connection-table-order", err); + } + return []; + }, + }, + ); + + const baseColumns = useMemo(() => { return [ { field: "host", @@ -248,6 +319,49 @@ export const ConnectionTable = (props: Props) => { ]; }, [columnWidths, t]); + useEffect(() => { + setColumnOrder((prevValue) => { + const baseFields = baseColumns.map((col) => col.field); + const prev = Array.isArray(prevValue) ? prevValue : []; + const reconciled = reconcileColumnOrder(prev, baseFields); + if ( + reconciled.length === prev.length && + reconciled.every((field, i) => field === prev[i]) + ) { + return prevValue; + } + return reconciled; + }); + }, [baseColumns, setColumnOrder]); + + const columns = useMemo(() => { + const order = Array.isArray(columnOrder) ? columnOrder : []; + const orderMap = new Map(order.map((field, index) => [field, index])); + + return [...baseColumns].sort((a, b) => { + const aIndex = orderMap.has(a.field) + ? (orderMap.get(a.field) as number) + : Number.MAX_SAFE_INTEGER; + const bIndex = orderMap.has(b.field) + ? (orderMap.get(b.field) as number) + : Number.MAX_SAFE_INTEGER; + + if (aIndex === bIndex) { + return order.indexOf(a.field) - order.indexOf(b.field); + } + + return aIndex - bIndex; + }); + }, [baseColumns, columnOrder]); + + const visibleColumnsCount = useMemo(() => { + return columns.reduce((count, column) => { + return (columnVisibilityModel?.[column.field] ?? true) !== false + ? count + 1 + : count; + }, 0); + }, [columns, columnVisibilityModel]); + const handleColumnResize = (params: GridColumnResizeParams) => { const { colDef, width } = params; setColumnWidths((prev) => ({ @@ -256,6 +370,111 @@ export const ConnectionTable = (props: Props) => { })); }; + const handleColumnVisibilityChange = useCallback( + (model: GridColumnVisibilityModel) => { + const hiddenFields = new Set(); + Object.entries(model).forEach(([field, value]) => { + if (value === false) { + hiddenFields.add(field); + } + }); + + const nextVisibleCount = columns.reduce((count, column) => { + return hiddenFields.has(column.field) ? count : count + 1; + }, 0); + + if (nextVisibleCount === 0) { + return; + } + + setColumnVisibilityModel(() => { + const sanitized: Partial> = {}; + hiddenFields.forEach((field) => { + sanitized[field] = false; + }); + return sanitized; + }); + }, + [columns, setColumnVisibilityModel], + ); + + const handleToggleColumn = useCallback( + (field: string, visible: boolean) => { + if (!visible && visibleColumnsCount <= 1) { + return; + } + + setColumnVisibilityModel((prev) => { + const next = { ...(prev ?? {}) }; + if (visible) { + delete next[field]; + } else { + next[field] = false; + } + return next; + }); + }, + [setColumnVisibilityModel, visibleColumnsCount], + ); + + const handleColumnOrderChange = useCallback( + (params: GridColumnOrderChangeParams) => { + setColumnOrder((prevValue) => { + const baseFields = baseColumns.map((col) => col.field); + const currentOrder = Array.isArray(prevValue) + ? [...prevValue] + : [...baseFields]; + const field = params.column.field; + const currentIndex = currentOrder.indexOf(field); + if (currentIndex === -1) return currentOrder; + + currentOrder.splice(currentIndex, 1); + const targetIndex = Math.min( + Math.max(params.targetIndex, 0), + currentOrder.length, + ); + currentOrder.splice(targetIndex, 0, field); + + return currentOrder; + }); + }, + [baseColumns, setColumnOrder], + ); + + const handleManagerOrderChange = useCallback( + (order: string[]) => { + setColumnOrder(() => { + const baseFields = baseColumns.map((col) => col.field); + return reconcileColumnOrder(order, baseFields); + }); + }, + [baseColumns, setColumnOrder], + ); + + const handleResetColumns = useCallback(() => { + setColumnVisibilityModel({}); + setColumnOrder(baseColumns.map((col) => col.field)); + }, [baseColumns, setColumnOrder, setColumnVisibilityModel]); + + const gridVisibilityModel = useMemo(() => { + const result: GridColumnVisibilityModel = {}; + if (!columnVisibilityModel) return result; + Object.entries(columnVisibilityModel).forEach(([field, value]) => { + if (typeof value === "boolean") { + result[field] = value; + } + }); + return result; + }, [columnVisibilityModel]); + + const columnOptions = useMemo(() => { + return columns.map((column) => ({ + field: column.field, + label: column.headerName ?? column.field, + visible: (columnVisibilityModel?.[column.field] ?? true) !== false, + })); + }, [columns, columnVisibilityModel]); + const connRows = useMemo(() => { return connections.map((each) => { const { metadata, rulePayload } = each; @@ -286,24 +505,98 @@ export const ConnectionTable = (props: Props) => { }, [connections]); return ( - onShowDetail(e.row.connectionData)} - density="compact" - sx={{ - border: "none", - "div:focus": { outline: "none !important" }, - "& .MuiDataGrid-columnHeader": { - userSelect: "none", - }, - }} - columnVisibilityModel={columnVisible} - onColumnVisibilityModelChange={(e) => setColumnVisible(e)} - onColumnResize={handleColumnResize} - disableColumnMenu={false} - /> + + + onShowDetail(e.row.connectionData)} + density="compact" + sx={{ + flex: 1, + border: "none", + minHeight: 0, + "div:focus": { outline: "none !important" }, + "& .MuiDataGrid-columnHeader": { + userSelect: "none", + }, + }} + columnVisibilityModel={gridVisibilityModel} + onColumnVisibilityModelChange={handleColumnVisibilityChange} + onColumnResize={handleColumnResize} + onColumnOrderChange={handleColumnOrderChange} + slotProps={{ + columnMenu: { + slots: { + columnMenuColumnsItem: ConnectionColumnMenuColumnsItem, + }, + }, + }} + /> + + + + ); +}; + +type ConnectionColumnMenuManageItemProps = GridColumnMenuItemProps & { + onOpenColumnManager: () => void; +}; + +const ConnectionColumnMenuManageItem = ( + props: ConnectionColumnMenuManageItemProps, +) => { + const { onClick, onOpenColumnManager } = props; + const rootProps = useGridRootProps(); + const { t } = useTranslation(); + const handleClick = useCallback( + (event: MouseEvent) => { + onClick(event); + onOpenColumnManager(); + }, + [onClick, onOpenColumnManager], + ); + + if (rootProps.disableColumnSelector) { + return null; + } + + const MenuItem = rootProps.slots.baseMenuItem; + const Icon = rootProps.slots.columnMenuManageColumnsIcon; + + return ( + }> + {t("connections.components.columnManager.title")} + + ); +}; + +const ConnectionColumnMenuColumnsItem = (props: GridColumnMenuItemProps) => { + const onOpenColumnManager = use(ColumnManagerContext); + + return ( + <> + + + ); }; diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index a77f4db85..0cb5d43cf 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -77,6 +77,17 @@ const GRAPH_CONFIG = { }, }; +const MIN_FPS = 8; +const MAX_FPS = 20; +const FPS_ADJUST_INTERVAL = 3000; // ms +const FPS_SAMPLE_WINDOW = 12; +const STALE_DATA_THRESHOLD = 2500; // ms without fresh data => drop FPS +const RESUME_FPS_TARGET = 12; +const RESUME_COOLDOWN_MS = 2000; + +const getNow = () => + typeof performance !== "undefined" ? performance.now() : Date.now(); + interface EnhancedCanvasTrafficGraphProps { ref?: Ref; } @@ -105,6 +116,10 @@ export const EnhancedCanvasTrafficGraph = memo( const [timeRange, setTimeRange] = useState(10); const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); + const initialFocusState = + typeof document !== "undefined" ? !document.hidden : true; + const [isWindowFocused, setIsWindowFocused] = useState(initialFocusState); + // 悬浮提示状态 const [tooltipData, setTooltipData] = useState({ x: 0, @@ -122,6 +137,18 @@ export const EnhancedCanvasTrafficGraph = memo( const animationFrameRef = useRef(undefined); const lastRenderTimeRef = useRef(0); const isInitializedRef = useRef(false); + const isWindowFocusedRef = useRef(initialFocusState); + const fpsControllerRef = useRef<{ + target: number; + samples: number[]; + lastAdjustTime: number; + }>({ + target: GRAPH_CONFIG.targetFPS, + samples: [], + lastAdjustTime: 0, + }); + const lastDataTimestampRef = useRef(0); + const resumeCooldownRef = useRef(0); // 当前显示的数据缓存 const [displayData, dispatchDisplayData] = useReducer( @@ -129,6 +156,7 @@ export const EnhancedCanvasTrafficGraph = memo( [], ); const debounceTimeoutRef = useRef(null); + const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS); // 主题颜色配置 const colors = useMemo( @@ -165,6 +193,74 @@ export const EnhancedCanvasTrafficGraph = memo( }; }, [dataPoints, timeRange, getDataForTimeRange, updateDisplayData]); + useEffect(() => { + if (displayData.length === 0) { + lastDataTimestampRef.current = 0; + fpsControllerRef.current.target = GRAPH_CONFIG.targetFPS; + fpsControllerRef.current.samples = []; + fpsControllerRef.current.lastAdjustTime = 0; + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect + setCurrentFPS(GRAPH_CONFIG.targetFPS); + return; + } + + const latestTimestamp = + displayData[displayData.length - 1]?.timestamp ?? null; + if (latestTimestamp) { + lastDataTimestampRef.current = latestTimestamp; + } + }, [displayData]); + + const handleFocusStateChange = useCallback( + (focused: boolean) => { + isWindowFocusedRef.current = focused; + setIsWindowFocused(focused); + + const highResNow = getNow(); + lastRenderTimeRef.current = highResNow; + + if (focused) { + resumeCooldownRef.current = Date.now(); + const controller = fpsControllerRef.current; + const resumeTarget = Math.max( + MIN_FPS, + Math.min(controller.target, RESUME_FPS_TARGET), + ); + controller.target = resumeTarget; + controller.samples = []; + controller.lastAdjustTime = 0; + setCurrentFPS(resumeTarget); + } else { + resumeCooldownRef.current = 0; + } + }, + [setIsWindowFocused, setCurrentFPS], + ); + + useEffect(() => { + if (typeof window === "undefined" || typeof document === "undefined") { + return; + } + + const handleFocus = () => handleFocusStateChange(true); + const handleBlur = () => handleFocusStateChange(false); + const handleVisibilityChange = () => + handleFocusStateChange(!document.hidden); + + window.addEventListener("focus", handleFocus); + window.addEventListener("blur", handleBlur); + document.addEventListener("visibilitychange", handleVisibilityChange); + + return () => { + window.removeEventListener("focus", handleFocus); + window.removeEventListener("blur", handleBlur); + document.removeEventListener( + "visibilitychange", + handleVisibilityChange, + ); + }; + }, [handleFocusStateChange]); + // Y轴坐标计算 - 基于刻度范围的线性映射 const calculateY = useCallback( (value: number, height: number, data: ITrafficDataPoint[]): number => { @@ -792,31 +888,135 @@ export const EnhancedCanvasTrafficGraph = memo( tooltipData, ]); + const collectFrameSample = useCallback( + (renderDuration: number, frameBudget: number) => { + const controller = fpsControllerRef.current; + controller.samples.push(renderDuration); + if (controller.samples.length > FPS_SAMPLE_WINDOW) { + controller.samples.shift(); + } + + const perfNow = getNow(); + const lastDataAge = + lastDataTimestampRef.current > 0 + ? Date.now() - lastDataTimestampRef.current + : null; + const isDataStale = + typeof lastDataAge === "number" && lastDataAge > STALE_DATA_THRESHOLD; + + let inResumeCooldown = false; + if (resumeCooldownRef.current) { + const elapsedSinceResume = Date.now() - resumeCooldownRef.current; + if (elapsedSinceResume < RESUME_COOLDOWN_MS) { + inResumeCooldown = true; + } else { + resumeCooldownRef.current = 0; + } + } + + if (isDataStale && controller.target !== MIN_FPS) { + controller.target = MIN_FPS; + controller.samples = []; + controller.lastAdjustTime = perfNow; + setCurrentFPS(controller.target); + return; + } + + if ( + !isDataStale && + !inResumeCooldown && + controller.target < GRAPH_CONFIG.targetFPS + ) { + controller.target = Math.min( + GRAPH_CONFIG.targetFPS, + controller.target + 2, + ); + controller.samples = []; + controller.lastAdjustTime = perfNow; + setCurrentFPS(controller.target); + } + + if ( + controller.lastAdjustTime !== 0 && + perfNow - controller.lastAdjustTime < FPS_ADJUST_INTERVAL + ) { + return; + } + + if (controller.samples.length === 0) return; + + const avgRender = + controller.samples.reduce((sum, value) => sum + value, 0) / + controller.samples.length; + + let nextTarget = controller.target; + + if (avgRender > frameBudget * 0.75 && controller.target > MIN_FPS) { + nextTarget = Math.max(MIN_FPS, controller.target - 2); + } else if ( + avgRender < Math.max(4, frameBudget * 0.4) && + controller.target < MAX_FPS && + !inResumeCooldown + ) { + nextTarget = Math.min(MAX_FPS, controller.target + 2); + } + + controller.samples = []; + controller.lastAdjustTime = perfNow; + + if (nextTarget !== controller.target) { + controller.target = nextTarget; + setCurrentFPS(nextTarget); + } + }, + [setCurrentFPS], + ); + // 受控的动画循环 useEffect(() => { - const animate = (currentTime: number) => { - // 控制帧率,减少不必要的重绘 - if ( - currentTime - lastRenderTimeRef.current >= - 1000 / GRAPH_CONFIG.targetFPS - ) { - drawGraph(); - lastRenderTimeRef.current = currentTime; + if (!isWindowFocused || displayData.length === 0) { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; } + lastRenderTimeRef.current = getNow(); + return; + } + + const animate = (currentTime: number) => { + if (!isWindowFocusedRef.current) { + lastRenderTimeRef.current = getNow(); + animationFrameRef.current = undefined; + return; + } + + const targetFPS = fpsControllerRef.current.target; + const frameBudget = 1000 / targetFPS; + + if ( + currentTime - lastRenderTimeRef.current >= frameBudget || + !isInitializedRef.current + ) { + const drawStart = getNow(); + drawGraph(); + const drawEnd = getNow(); + + lastRenderTimeRef.current = currentTime; + collectFrameSample(drawEnd - drawStart, frameBudget); + } + animationFrameRef.current = requestAnimationFrame(animate); }; - // 只有在有数据时才开始动画 - if (displayData.length > 0) { - animationFrameRef.current = requestAnimationFrame(animate); - } + animationFrameRef.current = requestAnimationFrame(animate); return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = undefined; } }; - }, [drawGraph, displayData.length]); + }, [drawGraph, displayData.length, isWindowFocused, collectFrameSample]); // 切换时间范围 const handleTimeRangeClick = useCallback((event: React.MouseEvent) => { @@ -977,7 +1177,7 @@ export const EnhancedCanvasTrafficGraph = memo( }} > Points: {displayData.length} | Compressed:{" "} - {samplerStats.compressedBufferSize} + {samplerStats.compressedBufferSize} | FPS: {currentFPS} {/* 悬浮提示框 */} diff --git a/src/components/home/ip-info-card.tsx b/src/components/home/ip-info-card.tsx index a8f81d381..18360e550 100644 --- a/src/components/home/ip-info-card.tsx +++ b/src/components/home/ip-info-card.tsx @@ -1,11 +1,11 @@ import { LocationOnOutlined, RefreshOutlined, - VisibilityOutlined, VisibilityOffOutlined, + VisibilityOutlined, } from "@mui/icons-material"; -import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material"; -import { useState, useEffect, useCallback, memo } from "react"; +import { Box, Button, IconButton, Skeleton, Typography } from "@mui/material"; +import { memo, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { getIpInfo } from "@/services/api"; diff --git a/src/components/setting/mods/auto-backup-settings.tsx b/src/components/setting/mods/auto-backup-settings.tsx new file mode 100644 index 000000000..c9736ec7a --- /dev/null +++ b/src/components/setting/mods/auto-backup-settings.tsx @@ -0,0 +1,205 @@ +import { + InputAdornment, + ListItem, + ListItemText, + Stack, + TextField, +} from "@mui/material"; +import { useLockFn } from "ahooks"; +import { Fragment, useMemo, useState, type ChangeEvent } from "react"; +import { useTranslation } from "react-i18next"; + +import { Switch } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; +import { showNotice } from "@/services/noticeService"; + +const MIN_INTERVAL_HOURS = 1; +const MAX_INTERVAL_HOURS = 168; + +interface AutoBackupState { + scheduleEnabled: boolean; + intervalHours: number; + changeEnabled: boolean; +} + +export function AutoBackupSettings() { + const { t } = useTranslation(); + const { verge, patchVerge } = useVerge(); + const derivedValues = useMemo(() => { + return { + scheduleEnabled: verge?.enable_auto_backup_schedule ?? false, + intervalHours: verge?.auto_backup_interval_hours ?? 24, + changeEnabled: verge?.auto_backup_on_change ?? true, + }; + }, [ + verge?.enable_auto_backup_schedule, + verge?.auto_backup_interval_hours, + verge?.auto_backup_on_change, + ]); + const [pendingValues, setPendingValues] = useState( + null, + ); + const values = useMemo(() => { + if (!pendingValues) { + return derivedValues; + } + if ( + pendingValues.scheduleEnabled === derivedValues.scheduleEnabled && + pendingValues.intervalHours === derivedValues.intervalHours && + pendingValues.changeEnabled === derivedValues.changeEnabled + ) { + return derivedValues; + } + return pendingValues; + }, [pendingValues, derivedValues]); + const [intervalInputDraft, setIntervalInputDraft] = useState( + null, + ); + + const applyPatch = useLockFn( + async ( + partial: Partial, + payload: Partial, + ) => { + const nextValues = { ...values, ...partial }; + setPendingValues(nextValues); + try { + await patchVerge(payload); + } catch (error) { + showNotice.error(error); + setPendingValues(null); + } + }, + ); + + const disabled = !verge; + + const handleScheduleToggle = ( + _: ChangeEvent, + checked: boolean, + ) => { + applyPatch( + { scheduleEnabled: checked }, + { + enable_auto_backup_schedule: checked, + auto_backup_interval_hours: values.intervalHours, + }, + ); + }; + + const handleChangeToggle = ( + _: ChangeEvent, + checked: boolean, + ) => { + applyPatch({ changeEnabled: checked }, { auto_backup_on_change: checked }); + }; + + const handleIntervalInputChange = (event: ChangeEvent) => { + setIntervalInputDraft(event.target.value); + }; + + const commitIntervalInput = () => { + const rawValue = intervalInputDraft ?? values.intervalHours.toString(); + const trimmed = rawValue.trim(); + if (trimmed === "") { + setIntervalInputDraft(null); + return; + } + + const parsed = Number(trimmed); + if (!Number.isFinite(parsed)) { + setIntervalInputDraft(null); + return; + } + + const clamped = Math.min( + MAX_INTERVAL_HOURS, + Math.max(MIN_INTERVAL_HOURS, Math.round(parsed)), + ); + + if (clamped === values.intervalHours) { + setIntervalInputDraft(null); + return; + } + + applyPatch( + { intervalHours: clamped }, + { auto_backup_interval_hours: clamped }, + ); + setIntervalInputDraft(null); + }; + + const scheduleDisabled = disabled || !values.scheduleEnabled; + + return ( + + + + + + + + + + + + { + if (event.key === "Enter") { + event.preventDefault(); + commitIntervalInput(); + } + }} + sx={{ minWidth: 160 }} + slotProps={{ + input: { + endAdornment: ( + + {t("shared.units.hours")} + + ), + }, + htmlInput: { + min: MIN_INTERVAL_HOURS, + max: MAX_INTERVAL_HOURS, + inputMode: "numeric", + }, + }} + /> + + + + + + + + + + + ); +} diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index b1323db6f..21ce89c14 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -58,14 +58,6 @@ export const BackupConfigViewer = memo( webdav_username !== username || webdav_password !== password; - console.log( - "webdavChanged", - webdavChanged, - webdav_url, - webdav_username, - webdav_password, - ); - const handleClickShowPassword = () => { setShowPassword((prev) => !prev); }; diff --git a/src/components/setting/mods/backup-history-viewer.tsx b/src/components/setting/mods/backup-history-viewer.tsx new file mode 100644 index 000000000..4a63fc743 --- /dev/null +++ b/src/components/setting/mods/backup-history-viewer.tsx @@ -0,0 +1,344 @@ +import DeleteOutline from "@mui/icons-material/DeleteOutline"; +import DownloadRounded from "@mui/icons-material/DownloadRounded"; +import RefreshRounded from "@mui/icons-material/RefreshRounded"; +import RestoreRounded from "@mui/icons-material/RestoreRounded"; +import { + Box, + Button, + IconButton, + List, + ListItem, + ListItemText, + ListSubheader, + Stack, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { save } from "@tauri-apps/plugin-dialog"; +import { useLockFn } from "ahooks"; +import dayjs from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; +import { useVerge } from "@/hooks/use-verge"; +import { + deleteLocalBackup, + deleteWebdavBackup, + exportLocalBackup, + listLocalBackup, + listWebDavBackup, + restartApp, + restoreLocalBackup, + restoreWebDavBackup, +} from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +dayjs.extend(customParseFormat); +dayjs.extend(relativeTime); + +const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; +const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; + +type BackupSource = "local" | "webdav"; + +interface BackupHistoryViewerProps { + open: boolean; + source: BackupSource; + page: number; + onSourceChange: (source: BackupSource) => void; + onPageChange: (page: number) => void; + onClose: () => void; +} + +interface BackupRow { + filename: string; + platform: string; + backup_time: dayjs.Dayjs; +} + +const confirmAsync = async (message: string) => { + const fn = window.confirm as (msg?: string) => boolean; + return fn(message); +}; + +export const BackupHistoryViewer = ({ + open, + source, + page, + onSourceChange, + onPageChange, + onClose, +}: BackupHistoryViewerProps) => { + const { t } = useTranslation(); + const { verge } = useVerge(); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [isRestarting, setIsRestarting] = useState(false); + const isLocal = source === "local"; + const isWebDavConfigured = Boolean( + verge?.webdav_url && verge?.webdav_username && verge?.webdav_password, + ); + const shouldSkipWebDav = !isLocal && !isWebDavConfigured; + const pageSize = 8; + const isBusy = loading || isRestarting; + + const buildRow = useCallback((filename: string): BackupRow | null => { + const platform = filename.split("-")[0]; + const match = filename.match(FILENAME_PATTERN); + if (!match) return null; + return { + filename, + platform, + backup_time: dayjs(match[0], DATE_FORMAT), + }; + }, []); + + const fetchRows = useCallback(async () => { + if (!open) return; + if (shouldSkipWebDav) { + setRows([]); + return; + } + setLoading(true); + try { + const list = isLocal ? await listLocalBackup() : await listWebDavBackup(); + setRows( + list + .map((item) => buildRow(item.filename)) + .filter((item): item is BackupRow => item !== null) + .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)), + ); + } catch (error) { + console.error(error); + setRows([]); + showNotice.error(error); + } finally { + setLoading(false); + } + }, [buildRow, isLocal, open, shouldSkipWebDav]); + + useEffect(() => { + void fetchRows(); + }, [fetchRows]); + + const total = rows.length; + const pageCount = Math.max(1, Math.ceil(total / pageSize)); + const currentPage = Math.min(page, pageCount - 1); + const pagedRows = rows.slice( + currentPage * pageSize, + currentPage * pageSize + pageSize, + ); + + const summary = useMemo(() => { + if (shouldSkipWebDav) { + return t("settings.modals.backup.manual.webdav"); + } + if (!total) return t("settings.modals.backup.history.empty"); + const recent = rows[0]?.backup_time.fromNow(); + return t("settings.modals.backup.history.summary", { + count: total, + recent, + }); + }, [rows, shouldSkipWebDav, t, total]); + + const handleDelete = useLockFn(async (filename: string) => { + if (isRestarting) return; + if ( + !(await confirmAsync(t("settings.modals.backup.messages.confirmDelete"))) + ) + return; + if (isLocal) { + await deleteLocalBackup(filename); + } else { + await deleteWebdavBackup(filename); + } + await fetchRows(); + }); + + const handleRestore = useLockFn(async (filename: string) => { + if (isRestarting) return; + if ( + !(await confirmAsync(t("settings.modals.backup.messages.confirmRestore"))) + ) + return; + if (isLocal) { + await restoreLocalBackup(filename); + } else { + await restoreWebDavBackup(filename); + } + showNotice.success("settings.modals.backup.messages.restoreSuccess"); + setIsRestarting(true); + window.setTimeout(() => { + void restartApp().catch((err: unknown) => { + setIsRestarting(false); + showNotice.error(err); + }); + }, 1000); + }); + + const handleExport = useLockFn(async (filename: string) => { + if (isRestarting) return; + if (!isLocal) return; + const savePath = await save({ defaultPath: filename }); + if (!savePath || Array.isArray(savePath)) return; + try { + await exportLocalBackup(filename, savePath); + showNotice.success("settings.modals.backup.messages.localBackupExported"); + } catch (ignoreError: unknown) { + showNotice.error( + "settings.modals.backup.messages.localBackupExportFailed", + ); + } + }); + + const handleRefresh = () => { + if (isRestarting) return; + void fetchRows(); + }; + + return ( + + + + + + { + if (isBusy) return; + onSourceChange(val as BackupSource); + onPageChange(0); + }} + textColor="primary" + indicatorColor="primary" + > + + + + + + + + + {summary} + + + + {t("settings.modals.backup.history.title")} + + } + > + {pagedRows.length === 0 ? ( + + + + ) : ( + pagedRows.map((row) => ( + + {isLocal && ( + handleExport(row.filename)} + > + + + )} + handleDelete(row.filename)} + > + + + handleRestore(row.filename)} + > + + + + } + > + + + )) + )} + + + {pageCount > 1 && ( + + + {currentPage + 1} / {pageCount} + + + + + + + )} + + + + ); +}; diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx deleted file mode 100644 index 13c9f9824..000000000 --- a/src/components/setting/mods/backup-table-viewer.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import DeleteIcon from "@mui/icons-material/Delete"; -import DownloadIcon from "@mui/icons-material/Download"; -import RestoreIcon from "@mui/icons-material/Restore"; -import { - Box, - Paper, - IconButton, - Divider, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TablePagination, -} from "@mui/material"; -import { Typography } from "@mui/material"; -import { save } from "@tauri-apps/plugin-dialog"; -import { useLockFn } from "ahooks"; -import { Dayjs } from "dayjs"; -import { SVGProps, memo } from "react"; -import { useTranslation } from "react-i18next"; - -import { restartApp } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; - -export type BackupFile = { - platform: string; - backup_time: Dayjs; - allow_apply: boolean; - filename: string; -}; - -export const DEFAULT_ROWS_PER_PAGE = 5; - -type ConfirmFn = (message?: string) => boolean | Promise; - -// Normalizes synchronous and async confirm implementations. -const confirmAsync = async (message: string): Promise => { - const confirmFn = window.confirm as unknown as ConfirmFn; - return await confirmFn.call(window, message); -}; - -interface BackupTableViewerProps { - datasource: BackupFile[]; - page: number; - onPageChange: ( - event: React.MouseEvent | null, - page: number, - ) => void; - total: number; - onRefresh: () => Promise; - onDelete: (filename: string) => Promise; - onRestore: (filename: string) => Promise; - onExport?: (filename: string, destination: string) => Promise; -} - -export const BackupTableViewer = memo( - ({ - datasource, - page, - onPageChange, - total, - onRefresh, - onDelete, - onRestore, - onExport, - }: BackupTableViewerProps) => { - const { t } = useTranslation(); - - const handleDelete = useLockFn(async (filename: string) => { - await onDelete(filename); - await onRefresh(); - }); - - const handleRestore = useLockFn(async (filename: string) => { - await onRestore(filename).then(() => { - showNotice.success("settings.modals.backup.messages.restoreSuccess"); - }); - await restartApp(); - }); - - const handleExport = useLockFn(async (filename: string) => { - if (!onExport) { - return; - } - try { - const savePath = await save({ - defaultPath: filename, - }); - if (!savePath || Array.isArray(savePath)) { - return; - } - await onExport(filename, savePath); - showNotice.success( - "settings.modals.backup.messages.localBackupExported", - ); - } catch (error) { - console.error(error); - showNotice.error( - "settings.modals.backup.messages.localBackupExportFailed", - ); - } - }); - - return ( - - - - - - {t("settings.modals.backup.table.filename")} - - - {t("settings.modals.backup.table.backupTime")} - - - {t("settings.modals.backup.table.actions")} - - - - - {datasource.length > 0 ? ( - datasource.map((file) => { - const rowKey = `${file.platform}-${file.filename}-${file.backup_time.valueOf()}`; - return ( - - - {file.platform === "windows" ? ( - - ) : file.platform === "linux" ? ( - - ) : ( - - )} - {file.filename} - - - {file.backup_time.fromNow()} - - - - {onExport && ( - <> - { - e.preventDefault(); - await handleExport(file.filename); - }} - > - - - - - )} - { - e.preventDefault(); - const confirmed = await confirmAsync( - t( - "settings.modals.backup.messages.confirmDelete", - ), - ); - if (confirmed) { - await handleDelete(file.filename); - } - }} - > - - - - { - e.preventDefault(); - const confirmed = await confirmAsync( - t( - "settings.modals.backup.messages.confirmRestore", - ), - ); - if (confirmed) { - await handleRestore(file.filename); - } - }} - > - - - - - - ); - }) - ) : ( - - - - - {t("settings.modals.backup.table.noBackups")} - - - - - )} - -
- -
- ); - }, -); - -function LinuxIcon(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - ); -} - -function WindowsIcon(props: SVGProps) { - return ( - - - - ); -} - -function MacIcon(props: SVGProps) { - return ( - - - - ); -} diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 3a1c1ecca..b9b62b76c 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -1,354 +1,213 @@ -import { Box, Button, Divider, Paper, Tab, Tabs } from "@mui/material"; -import dayjs from "dayjs"; -import customParseFormat from "dayjs/plugin/customParseFormat"; -import type { Ref } from "react"; +import { LoadingButton } from "@mui/lab"; import { - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useReducer, - useRef, - useState, -} from "react"; -import { createPortal } from "react-dom"; + Button, + List, + ListItem, + ListItemText, + Stack, + Typography, +} from "@mui/material"; +import { useLockFn } from "ahooks"; +import type { ReactNode, Ref } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base"; -import { - deleteLocalBackup, - deleteWebdavBackup, - listLocalBackup, - listWebDavBackup, - exportLocalBackup, - restoreLocalBackup, - restoreWebDavBackup, -} from "@/services/cmds"; +import { BaseDialog, DialogRef } from "@/components/base"; +import { createLocalBackup, createWebdavBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; -import { BackupConfigViewer } from "./backup-config-viewer"; -import { - BackupFile, - BackupTableViewer, - DEFAULT_ROWS_PER_PAGE, -} from "./backup-table-viewer"; -import { LocalBackupActions } from "./local-backup-actions"; -dayjs.extend(customParseFormat); +import { AutoBackupSettings } from "./auto-backup-settings"; +import { BackupHistoryViewer } from "./backup-history-viewer"; +import { BackupWebdavDialog } from "./backup-webdav-dialog"; -const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; -const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; type BackupSource = "local" | "webdav"; -type CloseButtonPosition = { top: number; left: number } | null; export function BackupViewer({ ref }: { ref?: Ref }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const contentRef = useRef(null); - const [dialogPaper, setDialogPaper] = useReducer( - (_: HTMLElement | null, next: HTMLElement | null) => next, - null as HTMLElement | null, - ); - const [closeButtonPosition, setCloseButtonPosition] = useReducer( - (_: CloseButtonPosition, next: CloseButtonPosition) => next, - null as CloseButtonPosition, - ); - - const [isLoading, setIsLoading] = useState(false); - const [backupFiles, setBackupFiles] = useState([]); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(0); - const [source, setSource] = useState("local"); + const [busyAction, setBusyAction] = useState(null); + const [historyOpen, setHistoryOpen] = useState(false); + const [historySource, setHistorySource] = useState("local"); + const [historyPage, setHistoryPage] = useState(0); + const [webdavDialogOpen, setWebdavDialogOpen] = useState(false); useImperativeHandle(ref, () => ({ - open: () => { - setOpen(true); - }, + open: () => setOpen(true), close: () => setOpen(false), })); - // Handle page change - const handleChangePage = useCallback( - (_: React.MouseEvent | null, page: number) => { - setPage(page); - }, - [], - ); + const openHistory = (target: BackupSource) => { + setHistorySource(target); + setHistoryPage(0); + setHistoryOpen(true); + }; - const handleChangeSource = useCallback( - (_event: React.SyntheticEvent, newSource: string) => { - setSource(newSource as BackupSource); - setPage(0); - }, - [], - ); - - const buildBackupFile = useCallback((filename: string) => { - const platform = filename.split("-")[0]; - const fileBackupTimeStr = filename.match(FILENAME_PATTERN); - - if (fileBackupTimeStr === null) { - return null; - } - - const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); - const allowApply = true; - return { - filename, - platform, - backup_time: backupTime, - allow_apply: allowApply, - } as BackupFile; - }, []); - - const getAllBackupFiles = useCallback(async (): Promise => { - if (source === "local") { - const files = await listLocalBackup(); - return files - .map((file) => buildBackupFile(file.filename)) - .filter((item): item is BackupFile => item !== null) - .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); - } - - const files = await listWebDavBackup(); - return files - .map((file) => { - return buildBackupFile(file.filename); - }) - .filter((item): item is BackupFile => item !== null) - .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); - }, [buildBackupFile, source]); - - const fetchAndSetBackupFiles = useCallback(async () => { + const handleBackup = useLockFn(async (target: BackupSource) => { try { - setIsLoading(true); - const files = await getAllBackupFiles(); - setBackupFiles(files); - setTotal(files.length); + setBusyAction(target); + if (target === "local") { + await createLocalBackup(); + showNotice.success( + "settings.modals.backup.messages.localBackupCreated", + ); + } else { + await createWebdavBackup(); + showNotice.success("settings.modals.backup.messages.backupCreated"); + } } catch (error) { - setBackupFiles([]); - setTotal(0); console.error(error); + showNotice.error( + target === "local" + ? "settings.modals.backup.messages.localBackupFailed" + : "settings.modals.backup.messages.backupFailed", + target === "local" ? undefined : { error }, + ); } finally { - setIsLoading(false); + setBusyAction(null); } - }, [getAllBackupFiles]); - - useEffect(() => { - if (open) { - fetchAndSetBackupFiles(); - const paper = contentRef.current?.closest(".MuiPaper-root"); - setDialogPaper((paper as HTMLElement) ?? null); - } else { - setDialogPaper(null); - } - }, [open, fetchAndSetBackupFiles]); - - useEffect(() => { - if (!open || dialogPaper) { - return; - } - const frame = requestAnimationFrame(() => { - const paper = contentRef.current?.closest(".MuiPaper-root"); - setDialogPaper((paper as HTMLElement) ?? null); - }); - return () => cancelAnimationFrame(frame); - }, [open, dialogPaper]); - - useEffect(() => { - if (!dialogPaper) { - setCloseButtonPosition(null); - return; - } - if (typeof window === "undefined") { - return; - } - - const updatePosition = () => { - const rect = dialogPaper.getBoundingClientRect(); - setCloseButtonPosition({ - top: rect.bottom - 16, - left: rect.right - 24, - }); - }; - - updatePosition(); - - let resizeObserver: ResizeObserver | null = null; - if (typeof ResizeObserver !== "undefined") { - resizeObserver = new ResizeObserver(() => { - updatePosition(); - }); - resizeObserver.observe(dialogPaper); - } - - const scrollTargets: EventTarget[] = []; - const addScrollListener = (target: EventTarget | null) => { - if (!target) { - return; - } - target.addEventListener("scroll", updatePosition, true); - scrollTargets.push(target); - }; - - addScrollListener(window); - addScrollListener(dialogPaper); - const dialogContent = dialogPaper.querySelector(".MuiDialogContent-root"); - addScrollListener(dialogContent); - - window.addEventListener("resize", updatePosition); - - return () => { - resizeObserver?.disconnect(); - scrollTargets.forEach((target) => { - target.removeEventListener("scroll", updatePosition, true); - }); - window.removeEventListener("resize", updatePosition); - }; - }, [dialogPaper]); - - const handleDelete = useCallback( - async (filename: string) => { - if (source === "local") { - await deleteLocalBackup(filename); - } else { - await deleteWebdavBackup(filename); - } - }, - [source], - ); - - const handleRestore = useCallback( - async (filename: string) => { - if (source === "local") { - await restoreLocalBackup(filename); - } else { - await restoreWebDavBackup(filename); - } - }, - [source], - ); - - const handleExport = useCallback( - async (filename: string, destination: string) => { - await exportLocalBackup(filename, destination); - }, - [], - ); - - const dataSource = useMemo( - () => - backupFiles.slice( - page * DEFAULT_ROWS_PER_PAGE, - page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE, - ), - [backupFiles, page], - ); + }); return ( setOpen(false)} onClose={() => setOpen(false)} - disableFooter > - - - + `1px solid ${theme.palette.divider}`, + borderRadius: 2, + p: 2, }} > - - - - - {source === "local" ? ( - - ) : ( - - )} - - - - - - - {dialogPaper && - closeButtonPosition && - createPortal( - theme.zIndex.modal + 1, - }} - > - - , - dialogPaper, - )} + + {t("settings.modals.backup.auto.title")} + + + + + + + `1px solid ${theme.palette.divider}`, + borderRadius: 2, + p: 2, + }} + > + + {t("settings.modals.backup.manual.title")} + + + {( + [ + { + key: "local" as BackupSource, + title: t("settings.modals.backup.tabs.local"), + description: t("settings.modals.backup.manual.local"), + actions: [ + handleBackup("local")} + > + {t("settings.modals.backup.actions.backup")} + , + , + ], + }, + { + key: "webdav" as BackupSource, + title: t("settings.modals.backup.tabs.webdav"), + description: t("settings.modals.backup.manual.webdav"), + actions: [ + handleBackup("webdav")} + > + {t("settings.modals.backup.actions.backup")} + , + , + , + ], + }, + ] satisfies Array<{ + key: BackupSource; + title: string; + description: string; + actions: ReactNode[]; + }> + ).map((item, idx) => ( + + + + + {item.actions} + + + + ))} + + + + + setHistoryOpen(false)} + /> + setWebdavDialogOpen(false)} + onBackupSuccess={() => openHistory("webdav")} + setBusy={(loading) => setBusyAction(loading ? "webdav" : null)} + /> ); } diff --git a/src/components/setting/mods/backup-webdav-dialog.tsx b/src/components/setting/mods/backup-webdav-dialog.tsx new file mode 100644 index 000000000..8ad0e17a0 --- /dev/null +++ b/src/components/setting/mods/backup-webdav-dialog.tsx @@ -0,0 +1,87 @@ +import { Box } from "@mui/material"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { BaseDialog, BaseLoadingOverlay } from "@/components/base"; +import { listWebDavBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +import { BackupConfigViewer } from "./backup-config-viewer"; + +interface BackupWebdavDialogProps { + open: boolean; + onClose: () => void; + onBackupSuccess?: () => void; + setBusy?: (loading: boolean) => void; +} + +export const BackupWebdavDialog = ({ + open, + onClose, + onBackupSuccess, + setBusy, +}: BackupWebdavDialogProps) => { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + + const handleLoading = useCallback( + (value: boolean) => { + setLoading(value); + setBusy?.(value); + }, + [setBusy], + ); + + const refreshWebdav = useCallback( + async (options?: { silent?: boolean }) => { + handleLoading(true); + try { + await listWebDavBackup(); + if (!options?.silent) { + showNotice.success( + "settings.modals.backup.messages.webdavRefreshSuccess", + ); + } + } catch (error) { + showNotice.error( + "settings.modals.backup.messages.webdavRefreshFailed", + { error }, + ); + } finally { + handleLoading(false); + } + }, + [handleLoading], + ); + + const refreshSilently = useCallback( + () => refreshWebdav({ silent: true }), + [refreshWebdav], + ); + + return ( + + + + { + await refreshSilently(); + onBackupSuccess?.(); + }} + onSaveSuccess={refreshSilently} + onRefresh={refreshWebdav} + onInit={refreshSilently} + /> + + + ); +}; diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 6f1482f4d..ebc9588df 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -372,7 +372,7 @@ export const LayoutViewer = forwardRef((_, ref) => { )} /> ((_, ref) => { await initIconPath(); onChangeData({ common_tray_icon: true }); patchVerge({ common_tray_icon: true }); - console.log(); } } }} diff --git a/src/components/setting/mods/local-backup-actions.tsx b/src/components/setting/mods/local-backup-actions.tsx deleted file mode 100644 index e2dd5bd50..000000000 --- a/src/components/setting/mods/local-backup-actions.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Button, Grid, Stack, Typography } from "@mui/material"; -import { useLockFn } from "ahooks"; -import { memo } from "react"; -import { useTranslation } from "react-i18next"; - -import { createLocalBackup } from "@/services/cmds"; -import { showNotice } from "@/services/noticeService"; - -interface LocalBackupActionsProps { - onBackupSuccess: () => Promise; - onRefresh: () => Promise; - setLoading: (loading: boolean) => void; -} - -export const LocalBackupActions = memo( - ({ onBackupSuccess, onRefresh, setLoading }: LocalBackupActionsProps) => { - const { t } = useTranslation(); - - const handleBackup = useLockFn(async () => { - try { - setLoading(true); - await createLocalBackup(); - showNotice.success( - "settings.modals.backup.messages.localBackupCreated", - ); - await onBackupSuccess(); - } catch (error) { - console.error(error); - showNotice.error("settings.modals.backup.messages.localBackupFailed"); - } finally { - setLoading(false); - } - }); - - const handleRefresh = useLockFn(async () => { - setLoading(true); - try { - await onRefresh(); - } finally { - setLoading(false); - } - }); - - return ( - - - - {t("settings.modals.backup.fields.info")} - - - - - - - - - - ); - }, -); diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index dcdf924c6..62109e070 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -59,6 +59,8 @@ export const MiscViewer = forwardRef((props, ref) => { try { await patchVerge({ app_log_level: values.appLogLevel, + app_log_max_size: values.appLogMaxSize, + app_log_max_count: values.appLogMaxCount, auto_close_connection: values.autoCloseConnection, auto_check_update: values.autoCheckUpdate, enable_builtin_enhanced: values.enableBuiltinEnhanced, diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index ad88bf120..be0db7d6e 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "إغلاق الاتصال" + }, + "columnManager": { + "title": "الأعمدة", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/ar/proxies.json b/src/locales/ar/proxies.json index 995a4d39f..071069f0c 100644 --- a/src/locales/ar/proxies.json +++ b/src/locales/ar/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 بروكسي السلسلة", + "toggleChain": "بروكسي السلسلة", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 4c01bff35..6a9ffcac3 100644 --- a/src/locales/ar/settings.json +++ b/src/locales/ar/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "حذف النسخة الاحتياطية", "restore": "استعادة", - "restoreBackup": "استعادة النسخة الاحتياطية" + "restoreBackup": "استعادة النسخة الاحتياطية", + "viewHistory": "View history" }, "fields": { "webdavUrl": "عنوان خادم WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "تمت الاستعادة بنجاح، سيعاد تشغيل التطبيق خلال ثانية واحدة", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "هل تريد بالتأكيد حذف ملف النسخة الاحتياطية هذا؟", "confirmRestore": "هل تريد بالتأكيد استعادة ملف النسخة الاحتياطية هذا؟" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "اسم الملف", "backupTime": "وقت النسخ الاحتياطي", diff --git a/src/locales/ar/shared.json b/src/locales/ar/shared.json index 604109371..9c4d4489b 100644 --- a/src/locales/ar/shared.json +++ b/src/locales/ar/shared.json @@ -21,7 +21,9 @@ "pause": "إيقاف مؤقت", "resume": "استأنف", "closeAll": "إغلاق الكل", - "clear": "مسح" + "clear": "مسح", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "التحديث عند", @@ -48,6 +50,7 @@ "milliseconds": "ميلي ثانية", "seconds": "ثواني", "minutes": "دقائق", + "hours": "ساعات", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index 97af29ee3..65d032fcf 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Verbindung schließen" + }, + "columnManager": { + "title": "Spalten", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/de/proxies.json b/src/locales/de/proxies.json index 022f60302..92e11588a 100644 --- a/src/locales/de/proxies.json +++ b/src/locales/de/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Ketten-Proxy", + "toggleChain": "Ketten-Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/de/settings.json b/src/locales/de/settings.json index 29a3cc39b..1cfa4c01f 100644 --- a/src/locales/de/settings.json +++ b/src/locales/de/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Sicherung löschen", "restore": "Wiederherstellen", - "restoreBackup": "Sicherung wiederherstellen" + "restoreBackup": "Sicherung wiederherstellen", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV-Serveradresse http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Wiederherstellung erfolgreich. Die App wird in 1 Sekunde neu starten.", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Dateiname", "backupTime": "Sicherungszeit", diff --git a/src/locales/de/shared.json b/src/locales/de/shared.json index 22684cae0..eb2e34d7d 100644 --- a/src/locales/de/shared.json +++ b/src/locales/de/shared.json @@ -21,7 +21,9 @@ "pause": "Pausieren", "resume": "Fortsetzen", "closeAll": "Alle schließen", - "clear": "Löschen" + "clear": "Löschen", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Aktualisiert am", @@ -48,6 +50,7 @@ "milliseconds": "Millisekunden", "seconds": "Sekunden", "minutes": "Minuten", + "hours": "Stunden", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 4d5e53605..713a8d114 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Close Connection" + }, + "columnManager": { + "title": "Columns", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/en/proxies.json b/src/locales/en/proxies.json index a359ee324..f7cba5d77 100644 --- a/src/locales/en/proxies.json +++ b/src/locales/en/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Chain Proxy", + "toggleChain": "Chain Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 661ab2f72..08cce1d21 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Delete Backup", "restore": "Restore", - "restoreBackup": "Restore Backup" + "restoreBackup": "Restore Backup", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV Server URL", @@ -306,9 +307,37 @@ "restoreSuccess": "Restore Success, App will restart in 1s", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Filename", "backupTime": "Backup Time", diff --git a/src/locales/en/shared.json b/src/locales/en/shared.json index 436164218..1e2f61317 100644 --- a/src/locales/en/shared.json +++ b/src/locales/en/shared.json @@ -21,7 +21,9 @@ "pause": "Pause", "resume": "Resume", "closeAll": "Close All", - "clear": "Clear" + "clear": "Clear", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Update At", @@ -48,6 +50,7 @@ "milliseconds": "ms", "seconds": "seconds", "minutes": "mins", + "hours": "hrs", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index e40c6b526..d398d03fe 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Cerrar conexión" + }, + "columnManager": { + "title": "Columnas", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/es/proxies.json b/src/locales/es/proxies.json index 1699dd5ae..719d4e5a3 100644 --- a/src/locales/es/proxies.json +++ b/src/locales/es/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Proxy en cadena", + "toggleChain": "Proxy en cadena", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index cbc1d05ab..a687ed588 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Eliminar copia de seguridad", "restore": "Restaurar", - "restoreBackup": "Restaurar copia de seguridad" + "restoreBackup": "Restaurar copia de seguridad", + "viewHistory": "View history" }, "fields": { "webdavUrl": "Dirección del servidor WebDAV http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Restauración exitosa. La aplicación se reiniciará en 1 segundo", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Nombre del archivo", "backupTime": "Tiempo de copia de seguridad", diff --git a/src/locales/es/shared.json b/src/locales/es/shared.json index cb06326a8..7ed7494c6 100644 --- a/src/locales/es/shared.json +++ b/src/locales/es/shared.json @@ -21,7 +21,9 @@ "pause": "Pausar", "resume": "Reanudar", "closeAll": "Cerrar todas", - "clear": "Limpiar" + "clear": "Limpiar", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Actualizado el", @@ -48,6 +50,7 @@ "milliseconds": "Milisegundos", "seconds": "Segundos", "minutes": "Minutos", + "hours": "Horas", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index dfd891c2a..3f7d62f18 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "بستن اتصال" + }, + "columnManager": { + "title": "ستون‌ها", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/fa/proxies.json b/src/locales/fa/proxies.json index 20bebdfa8..d2e7c0b6d 100644 --- a/src/locales/fa/proxies.json +++ b/src/locales/fa/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 پراکسی زنجیره‌ای", + "toggleChain": "پراکسی زنجیره‌ای", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json index 36a41769d..b99ec661e 100644 --- a/src/locales/fa/settings.json +++ b/src/locales/fa/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "حذف پشتیبان", "restore": "بازیابی", - "restoreBackup": "بازیابی پشتیبان" + "restoreBackup": "بازیابی پشتیبان", + "viewHistory": "View history" }, "fields": { "webdavUrl": "http(s):// URL سرور WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راه‌اندازی مجدد می‌شود", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "آیا از حذف این فایل پشتیبان اطمینان دارید؟", "confirmRestore": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "نام فایل", "backupTime": "زمان پشتیبان‌گیری", diff --git a/src/locales/fa/shared.json b/src/locales/fa/shared.json index 374707ac0..6ab4e72d0 100644 --- a/src/locales/fa/shared.json +++ b/src/locales/fa/shared.json @@ -21,7 +21,9 @@ "pause": "توقف", "resume": "از سرگیری", "closeAll": "بستن همه", - "clear": "پاک کردن" + "clear": "پاک کردن", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "به‌روزرسانی در", @@ -48,6 +50,7 @@ "milliseconds": "میلی‌ثانیه", "seconds": "ثانیه‌ها", "minutes": "دقیقه", + "hours": "ساعت", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 1616eeeb0..6b515227a 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Tutup Koneksi" + }, + "columnManager": { + "title": "Kolom", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/id/proxies.json b/src/locales/id/proxies.json index cc1d01776..26945b1d6 100644 --- a/src/locales/id/proxies.json +++ b/src/locales/id/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Proxy Rantai", + "toggleChain": "Proxy Rantai", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/id/settings.json b/src/locales/id/settings.json index 4196f7fa4..abdf1bea5 100644 --- a/src/locales/id/settings.json +++ b/src/locales/id/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Hapus Cadangan", "restore": "Pulihkan", - "restoreBackup": "Pulihkan Cadangan" + "restoreBackup": "Pulihkan Cadangan", + "viewHistory": "View history" }, "fields": { "webdavUrl": "URL Server WebDAV", @@ -306,9 +307,37 @@ "restoreSuccess": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Konfirmasi untuk menghapus file cadangan ini?", "confirmRestore": "Konfirmasi untuk memulihkan file cadangan ini?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Nama Berkas", "backupTime": "Waktu Cadangan", diff --git a/src/locales/id/shared.json b/src/locales/id/shared.json index 14eeec27a..057e9f74b 100644 --- a/src/locales/id/shared.json +++ b/src/locales/id/shared.json @@ -21,7 +21,9 @@ "pause": "Jeda", "resume": "Lanjut", "closeAll": "Tutup Semua", - "clear": "Bersihkan" + "clear": "Bersihkan", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Diperbarui Pada", @@ -48,6 +50,7 @@ "milliseconds": "milidetik", "seconds": "detik", "minutes": "menit", + "hours": "jam", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index 806f4bb13..6cde1afbd 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "接続を閉じる" + }, + "columnManager": { + "title": "列", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/jp/proxies.json b/src/locales/jp/proxies.json index 7667b6943..e46215b70 100644 --- a/src/locales/jp/proxies.json +++ b/src/locales/jp/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 チェーンプロキシ", + "toggleChain": "チェーンプロキシ", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/jp/settings.json b/src/locales/jp/settings.json index 51635416e..f02ec9b55 100644 --- a/src/locales/jp/settings.json +++ b/src/locales/jp/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "バックアップを削除", "restore": "復元", - "restoreBackup": "バックアップを復元" + "restoreBackup": "バックアップを復元", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAVサーバーのURL http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "復元に成功しました。アプリケーションは1秒後に再起動します。", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Confirm to delete this backup file?", "confirmRestore": "Confirm to restore this backup file?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "ファイル名", "backupTime": "バックアップ時間", diff --git a/src/locales/jp/shared.json b/src/locales/jp/shared.json index ac83ba54d..292e7c1b2 100644 --- a/src/locales/jp/shared.json +++ b/src/locales/jp/shared.json @@ -21,7 +21,9 @@ "pause": "一時停止", "resume": "再開", "closeAll": "すべて閉じる", - "clear": "クリア" + "clear": "クリア", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "更新日時", @@ -48,6 +50,7 @@ "milliseconds": "ミリ秒", "seconds": "秒", "minutes": "分", + "hours": "時間", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index f277d252a..036fa5853 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "연결 닫기" + }, + "columnManager": { + "title": "열", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/ko/proxies.json b/src/locales/ko/proxies.json index 09ce8674d..00e6b383e 100644 --- a/src/locales/ko/proxies.json +++ b/src/locales/ko/proxies.json @@ -6,7 +6,7 @@ "direct": "직접" }, "actions": { - "toggleChain": "🔗 체인 프록시", + "toggleChain": "체인 프록시", "connect": "연결", "disconnect": "연결 해제", "connecting": "연결 중...", diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 1fe918129..faa61a57c 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -285,7 +285,8 @@ "exportBackup": "백업 내보내기", "deleteBackup": "백업 삭제", "restore": "복원", - "restoreBackup": "백업 복원" + "restoreBackup": "백업 복원", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV 서버 URL", @@ -306,9 +307,37 @@ "restoreSuccess": "복원 성공, 1초 후 앱이 재시작됩니다", "localBackupExported": "로컬 백업이 내보내졌습니다", "localBackupExportFailed": "로컬 백업 내보내기 실패", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "이 백업 파일을 삭제하시겠습니까?", "confirmRestore": "이 백업 파일을 복원하시겠습니까?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "파일명", "backupTime": "백업 시간", diff --git a/src/locales/ko/shared.json b/src/locales/ko/shared.json index ca8e69842..4309e5f9c 100644 --- a/src/locales/ko/shared.json +++ b/src/locales/ko/shared.json @@ -21,7 +21,9 @@ "pause": "일시 정지", "resume": "재개", "closeAll": "모두 닫기", - "clear": "지우기" + "clear": "지우기", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "업데이트 시간", @@ -48,6 +50,7 @@ "milliseconds": "밀리초", "seconds": "초", "minutes": "분", + "hours": "시간", "kilobytes": "KB", "files": "파일" }, diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index 09d0caf92..24c706fbe 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Закрыть соединение" + }, + "columnManager": { + "title": "Столбцы", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/ru/proxies.json b/src/locales/ru/proxies.json index fca9ccf26..32224adff 100644 --- a/src/locales/ru/proxies.json +++ b/src/locales/ru/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Цепной прокси", + "toggleChain": "Цепной прокси", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 3b6551ea2..2a73cbf61 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Удалить резервную копию", "restore": "Восстановить", - "restoreBackup": "Восстановить резервную копию" + "restoreBackup": "Восстановить резервную копию", + "viewHistory": "View history" }, "fields": { "webdavUrl": "URL-адрес сервера WebDAV http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Вы уверены, что хотите удалить этот файл резервной копии?", "confirmRestore": "Вы уверены, что хотите восстановить этот файл резервной копии?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Имя файла", "backupTime": "Время резервного копирования", diff --git a/src/locales/ru/shared.json b/src/locales/ru/shared.json index e7c5c8617..be79ca589 100644 --- a/src/locales/ru/shared.json +++ b/src/locales/ru/shared.json @@ -21,7 +21,9 @@ "pause": "Пауза", "resume": "Возобновить", "closeAll": "Закрыть всё", - "clear": "Очистить" + "clear": "Очистить", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Обновлено в", @@ -48,6 +50,7 @@ "milliseconds": "миллисекунды", "seconds": "секунды", "minutes": "минуты", + "hours": "часы", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index e13e2bd18..040cae012 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Bağlantıyı Kapat" + }, + "columnManager": { + "title": "Sütunlar", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/tr/proxies.json b/src/locales/tr/proxies.json index ac098259b..89e71d09f 100644 --- a/src/locales/tr/proxies.json +++ b/src/locales/tr/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Zincir Proxy", + "toggleChain": "Zincir Proxy", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/tr/settings.json b/src/locales/tr/settings.json index 6c0adc844..2dee69253 100644 --- a/src/locales/tr/settings.json +++ b/src/locales/tr/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Yedeği Sil", "restore": "Geri Yükle", - "restoreBackup": "Yedeği Geri Yükle" + "restoreBackup": "Yedeği Geri Yükle", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV Sunucu URL'si", @@ -306,9 +307,37 @@ "restoreSuccess": "Geri Yükleme Başarılı, Uygulama 1 saniye içinde yeniden başlatılacak", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Bu yedek dosyasını silmeyi onaylıyor musunuz?", "confirmRestore": "Bu yedek dosyasını geri yüklemeyi onaylıyor musunuz?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Dosya Adı", "backupTime": "Yedekleme Zamanı", diff --git a/src/locales/tr/shared.json b/src/locales/tr/shared.json index f1ce80268..e094bdbc2 100644 --- a/src/locales/tr/shared.json +++ b/src/locales/tr/shared.json @@ -21,7 +21,9 @@ "pause": "Duraklat", "resume": "Sürdür", "closeAll": "Tümünü Kapat", - "clear": "Temizle" + "clear": "Temizle", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Güncelleme Zamanı", @@ -48,6 +50,7 @@ "milliseconds": "ms", "seconds": "saniye", "minutes": "dakika", + "hours": "saat", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index 847a07f1e..27a2476a9 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "Тоташуны ябу" + }, + "columnManager": { + "title": "Баганалар", + "dragHandle": "Drag handle" } } } diff --git a/src/locales/tt/proxies.json b/src/locales/tt/proxies.json index 202044318..b35b89172 100644 --- a/src/locales/tt/proxies.json +++ b/src/locales/tt/proxies.json @@ -6,7 +6,7 @@ "direct": "Direct" }, "actions": { - "toggleChain": "🔗 Чылбыр прокси", + "toggleChain": "Чылбыр прокси", "connect": "Connect", "disconnect": "Disconnect", "connecting": "Connecting...", diff --git a/src/locales/tt/settings.json b/src/locales/tt/settings.json index 692c0b550..231779998 100644 --- a/src/locales/tt/settings.json +++ b/src/locales/tt/settings.json @@ -285,7 +285,8 @@ "exportBackup": "Export Backup", "deleteBackup": "Резерв копияне бетерү", "restore": "Кайтару", - "restoreBackup": "Резерв копияне кайтару" + "restoreBackup": "Резерв копияне кайтару", + "viewHistory": "View history" }, "fields": { "webdavUrl": "WebDAV сервер URL-ы (http(s)://)", @@ -306,9 +307,37 @@ "restoreSuccess": "Уңышлы кайтарылды, кушымта 1 секундтан яңадан башланачак", "localBackupExported": "Local backup exported successfully", "localBackupExportFailed": "Failed to export local backup", + "webdavRefreshSuccess": "WebDAV refresh succeeded", + "webdavRefreshFailed": "WebDAV refresh failed: {{error}}", "confirmDelete": "Бу резерв копия файлын бетерергә телисезме?", "confirmRestore": "Бу резерв копия файлын кире кайтарырга телисезме?" }, + "auto": { + "title": "Automatic backup", + "scheduleLabel": "Enable scheduled backup", + "scheduleHelper": "Create local backups in the background at the configured interval.", + "intervalLabel": "Backup frequency", + "changeLabel": "Backup on critical changes", + "changeHelper": "Automatically back up when the global extend config/script or subscriptions are added, removed, or edited.", + "options": { + "hours": "Every {{n}} hours", + "days": "Every {{n}} days" + } + }, + "manual": { + "title": "Manual backup", + "local": "Creates a snapshot on this device, stored under the app data directory.", + "webdav": "Upload a snapshot to your WebDAV server once credentials are set.", + "configureWebdav": "Configure WebDAV" + }, + "history": { + "title": "Backup history", + "summary": "{{count}} backups • latest {{recent}}", + "empty": "No backups available" + }, + "webdav": { + "title": "WebDAV settings" + }, "table": { "filename": "Файл исеме", "backupTime": "Резерв копия вакыты", diff --git a/src/locales/tt/shared.json b/src/locales/tt/shared.json index 88199bd5f..9405b43ac 100644 --- a/src/locales/tt/shared.json +++ b/src/locales/tt/shared.json @@ -21,7 +21,9 @@ "pause": "Туктау", "resume": "Дәвам", "closeAll": "Барысын да ябу", - "clear": "Чистарту" + "clear": "Чистарту", + "previous": "Previous", + "next": "Next" }, "labels": { "updateAt": "Яңартылган вакыт", @@ -48,6 +50,7 @@ "milliseconds": "Миллисекундлар", "seconds": "Секундлар", "minutes": "Минутлар", + "hours": "Сәгатьләр", "kilobytes": "KB", "files": "Files" }, diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 3b4a4ddd2..2e498493f 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -23,6 +23,10 @@ }, "actions": { "closeConnection": "关闭连接" + }, + "columnManager": { + "title": "列设置", + "dragHandle": "拖拽控件" } } } diff --git a/src/locales/zh/proxies.json b/src/locales/zh/proxies.json index dbcfdc607..e95db1d0f 100644 --- a/src/locales/zh/proxies.json +++ b/src/locales/zh/proxies.json @@ -6,7 +6,7 @@ "direct": "直连" }, "actions": { - "toggleChain": "🔗 链式代理", + "toggleChain": "链式代理", "connect": "连接", "disconnect": "断开", "connecting": "连接中...", diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 368cdc4e4..3677fe1cf 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -285,7 +285,8 @@ "exportBackup": "导出备份", "deleteBackup": "删除备份", "restore": "恢复", - "restoreBackup": "恢复备份" + "restoreBackup": "恢复备份", + "viewHistory": "查看记录" }, "fields": { "webdavUrl": "WebDAV 服务器地址 http(s)://", @@ -306,9 +307,37 @@ "restoreSuccess": "恢复成功,应用将在 1 秒后重启", "localBackupExported": "本地备份导出成功", "localBackupExportFailed": "本地备份导出失败", + "webdavRefreshSuccess": "WebDAV 刷新成功", + "webdavRefreshFailed": "WebDAV 刷新失败: {{error}}", "confirmDelete": "确认删除此备份文件吗?", "confirmRestore": "确认恢复此份文件吗?" }, + "auto": { + "title": "自动备份", + "scheduleLabel": "启用定时备份", + "scheduleHelper": "按设定频率在后台创建本地备份文件。", + "intervalLabel": "备份频率", + "changeLabel": "关键变更时自动备份", + "changeHelper": "全局扩展配置/脚本或订阅增删改后会自动备份。", + "options": { + "hours": "每 {{n}} 小时", + "days": "每 {{n}} 天" + } + }, + "manual": { + "title": "手动备份", + "local": "在本设备的应用数据目录中创建备份。", + "webdav": "配置 WebDAV 后,可直接上传备份到服务器。", + "configureWebdav": "配置 WebDAV" + }, + "history": { + "title": "备份记录", + "summary": "共 {{count}} 份备份 · 最近 {{recent}}", + "empty": "暂无备份记录" + }, + "webdav": { + "title": "WebDAV 设置" + }, "table": { "filename": "文件名称", "backupTime": "备份时间", diff --git a/src/locales/zh/shared.json b/src/locales/zh/shared.json index 788e1d79e..7db791f85 100644 --- a/src/locales/zh/shared.json +++ b/src/locales/zh/shared.json @@ -21,7 +21,9 @@ "pause": "暂停", "resume": "继续", "closeAll": "关闭全部", - "clear": "清除" + "clear": "清除", + "previous": "上一页", + "next": "下一页" }, "labels": { "updateAt": "更新于", @@ -48,6 +50,7 @@ "milliseconds": "毫秒", "seconds": "秒", "minutes": "分钟", + "hours": "小时", "kilobytes": "KB", "files": "文件" }, diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 6da04e1a5..e2dca70c1 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -17,12 +17,16 @@ "type": "類型" }, "order": { - "default": "Default", + "default": "預設", "uploadSpeed": "上傳速度", "downloadSpeed": "下載速度" }, "actions": { "closeConnection": "關閉連線" + }, + "columnManager": { + "title": "欄位設定", + "dragHandle": "拖曳以移動" } } } diff --git a/src/locales/zhtw/home.json b/src/locales/zhtw/home.json index 5853832ca..e4d82121e 100644 --- a/src/locales/zhtw/home.json +++ b/src/locales/zhtw/home.json @@ -85,7 +85,7 @@ "unknown": "未知" }, "errors": { - "load": "獲取 IP 資訊失敗" + "load": "取得 IP 資訊失敗" } }, "currentProxy": { @@ -116,7 +116,7 @@ "download": "下載" }, "patterns": { - "minutes": "{{time}} Minutes" + "minutes": "{{time}} 分" } }, "clashMode": { diff --git a/src/locales/zhtw/proxies.json b/src/locales/zhtw/proxies.json index d6a010377..f4a1e32c4 100644 --- a/src/locales/zhtw/proxies.json +++ b/src/locales/zhtw/proxies.json @@ -6,7 +6,7 @@ "direct": "直連" }, "actions": { - "toggleChain": "🔗 鏈式代理", + "toggleChain": "鏈式代理", "connect": "連線", "disconnect": "中斷", "connecting": "連線中...", diff --git a/src/locales/zhtw/rules.json b/src/locales/zhtw/rules.json index 3eca5a4fd..c65bee98e 100644 --- a/src/locales/zhtw/rules.json +++ b/src/locales/zhtw/rules.json @@ -13,11 +13,11 @@ "feedback": { "notifications": { "provider": { - "updateSuccess": "{{name}} updated successfully", - "updateFailed": "Failed to update {{name}}: {{message}}", - "genericError": "Update failed: {{message}}", - "none": "No providers available to update", - "allUpdated": "All providers updated successfully" + "updateSuccess": "已成功更新 {{name}}", + "updateFailed": "更新 {{name}} 失敗:{{message}}", + "genericError": "更新失敗:{{message}}", + "none": "沒有可供更新的提供者", + "allUpdated": "所有提供者都已成功更新" } } }, diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index cfc6ab3fb..d78440f29 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -154,7 +154,7 @@ "tooltips": { "backupInfo": "支援本機或 WebDAV 方式備份配置檔案", "openConfDir": "如果軟體執行異常,!備份!並刪除此資料夾下的所有檔案,重新啟動軟體", - "liteMode": "關閉 GUI 介面,僅保留內核執行" + "liteMode": "關閉圖形介面,僅保留內核執行" }, "actions": { "copyVersion": "複製Verge版本號" @@ -192,10 +192,10 @@ "cssInjection": "CSS 注入" }, "actions": { - "editCss": "Edit CSS" + "editCss": "編輯 CSS" }, "dialogs": { - "editCssTitle": "Edit CSS" + "editCssTitle": "編輯 CSS" } }, "layout": { @@ -234,11 +234,11 @@ "clashPort": { "title": "連接埠設定", "fields": { - "mixed": "混合代理連接埠", - "socks": "SOCKS 代理連接埠", - "http": "HTTP(S) 代理連接埠", - "redir": "Redir 透明代理連接埠", - "tproxy": "Tproxy Port" + "mixed": "混合連接埠", + "socks": "SOCKS 連接埠", + "http": "HTTP(S) 連接埠", + "redir": "Redir 透明連接埠", + "tproxy": "Tproxy 連接埠" }, "actions": { "random": "隨機連接埠" @@ -285,7 +285,8 @@ "exportBackup": "匯出備份", "deleteBackup": "刪除備份", "restore": "還原", - "restoreBackup": "還原備份" + "restoreBackup": "還原備份", + "viewHistory": "檢視紀錄" }, "fields": { "webdavUrl": "WebDAV 伺服器位址 http(s)://", @@ -298,7 +299,7 @@ "usernameRequired": "使用者名稱不能為空", "passwordRequired": "密碼不能為空", "webdavConfigSaved": "WebDAV 配置儲存成功", - "webdavConfigSaveFailed": "儲存 WebDAV 配置失敗: {{error}}", + "webdavConfigSaveFailed": "儲存 WebDAV 設定檔失敗: {{error}}", "backupCreated": "備份建立成功", "backupFailed": "備份失敗: {{error}}", "localBackupCreated": "本機備份建立成功", @@ -306,15 +307,43 @@ "restoreSuccess": "還原成功,應用程式將在 1 秒後重啟", "localBackupExported": "本機備份匯出成功", "localBackupExportFailed": "本機備份匯出失敗", + "webdavRefreshSuccess": "WebDAV 更新成功", + "webdavRefreshFailed": "WebDAV 更新失敗: {{error}}", "confirmDelete": "確認是否刪除此備份檔案嗎?", "confirmRestore": "確認還原此份檔案嗎?" }, + "auto": { + "title": "自動備份", + "scheduleLabel": "啟用定時備份", + "scheduleHelper": "依設定頻率在背景建立本機備份檔案。", + "intervalLabel": "備份頻率", + "changeLabel": "關鍵變更時自動備份", + "changeHelper": "全域擴充配置/腳本或訂閱新增、刪除、修改時自動備份。", + "options": { + "hours": "每 {{n}} 小時", + "days": "每 {{n}} 天" + } + }, + "manual": { + "title": "手動備份", + "local": "在本機應用資料資料夾建立備份檔。", + "webdav": "設定 WebDAV 後,可直接上傳備份至伺服器。", + "configureWebdav": "設定 WebDAV" + }, + "history": { + "title": "備份紀錄", + "summary": "共 {{count}} 份備份 · 最近 {{recent}}", + "empty": "尚無備份紀錄" + }, + "webdav": { + "title": "WebDAV 設定" + }, "table": { "filename": "檔案名稱", "backupTime": "備份時間", "actions": "動作", "noBackups": "暫無備份", - "rowsPerPage": "Rows per page" + "rowsPerPage": "每頁列數" } }, "misc": { @@ -349,7 +378,7 @@ } }, "update": { - "title": "New Version v{{version}}", + "title": "新版本 v{{version}}", "actions": { "goToRelease": "前往發佈頁面", "update": "更新" @@ -401,7 +430,7 @@ "mtu": "最大傳輸單位" }, "tooltips": { - "dnsHijack": "Please use , to separate multiple DNS servers" + "dnsHijack": "請用半形逗號來區隔多個 DNS 伺服器" }, "messages": { "applied": "設定已套用" @@ -498,7 +527,7 @@ "configError": "DNS 設定錯誤:" }, "errors": { - "invalid": "Invalid configuration", + "invalid": "無效的設定", "invalidYaml": "YAML 格式無效" } }, diff --git a/src/locales/zhtw/shared.json b/src/locales/zhtw/shared.json index 48cb83c2f..c7d5fe72c 100644 --- a/src/locales/zhtw/shared.json +++ b/src/locales/zhtw/shared.json @@ -21,7 +21,9 @@ "pause": "暫停", "resume": "繼續", "closeAll": "關閉全部", - "clear": "清除" + "clear": "清除", + "previous": "上一頁", + "next": "下一頁" }, "labels": { "updateAt": "更新於", @@ -48,6 +50,7 @@ "milliseconds": "毫秒", "seconds": "秒", "minutes": "分鐘", + "hours": "小時", "kilobytes": "KB", "files": "檔案" }, diff --git a/src/pages/_layout/useLayoutEvents.ts b/src/pages/_layout/useLayoutEvents.ts index c26084a3f..0b7955b30 100644 --- a/src/pages/_layout/useLayoutEvents.ts +++ b/src/pages/_layout/useLayoutEvents.ts @@ -4,7 +4,6 @@ import { useEffect } from "react"; import { mutate } from "swr"; import { useListen } from "@/hooks/use-listen"; -import { getAxios } from "@/services/api"; export const useLayoutEvents = ( handleNotice: (payload: [string, string]) => void, @@ -39,7 +38,6 @@ export const useLayoutEvents = ( register( addListener("verge://refresh-clash-config", async () => { - await getAxios(true); mutate("getProxies"); mutate("getVersion"); mutate("getClashConfig"); diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 5286b73e2..4d2dce6ae 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -86,6 +86,7 @@ const ConnectionsPage = () => { const [isPaused, setIsPaused] = useState(false); const [frozenData, setFrozenData] = useState(null); + const [isColumnManagerOpen, setIsColumnManagerOpen] = useState(false); // 使用全局连接数据 const displayData = useMemo(() => { @@ -110,14 +111,17 @@ const ConnectionsPage = () => { const [filterConn] = useMemo(() => { const orderFunc = orderFunctionMap[curOrderOpt]; - let conns = displayData.connections?.filter((conn) => { - const { host, destinationIP, process } = conn.metadata; - return ( - match(host || "") || match(destinationIP || "") || match(process || "") - ); - }); - - if (orderFunc) conns = orderFunc(conns ?? []); + let conns: IConnectionsItem[] = (displayData.connections ?? []).filter( + (conn) => { + const { host, destinationIP, process } = conn.metadata; + return ( + match(host || "") || + match(destinationIP || "") || + match(process || "") + ); + }, + ); + if (orderFunc) conns = orderFunc(conns); return [conns]; }, [displayData, match, curOrderOpt]); @@ -145,6 +149,8 @@ const ConnectionsPage = () => { }); }, [connections]); + const hasTableData = filterConn.length > 0; + return ( { pt: 1, mb: 0.5, mx: "10px", - height: "36px", + minHeight: "36px", display: "flex", alignItems: "center", + gap: 1, userSelect: "text", position: "sticky", top: 0, @@ -235,15 +242,29 @@ const ConnectionsPage = () => { ))} )} - + *": { + flex: 1, + }, + }} + > + + - {!filterConn || filterConn.length === 0 ? ( + {!hasTableData ? ( ) : isTableLayout ? ( detailRef.current?.open(detail)} + columnManagerOpen={isTableLayout && isColumnManagerOpen} + onOpenColumnManager={() => setIsColumnManagerOpen(true)} + onCloseColumnManager={() => setIsColumnManagerOpen(false)} /> ) : ( { variant={isChainMode ? "contained" : "outlined"} onClick={onToggleChainMode} sx={{ ml: 1 }} + startIcon={ + isChainMode ? ( + + ) : ( + + ) + } > {t("proxies.page.actions.toggleChain")} diff --git a/src/services/api.ts b/src/services/api.ts index a5508509f..2ad41c272 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,44 +1,4 @@ import { fetch } from "@tauri-apps/plugin-http"; -import axios, { AxiosInstance } from "axios"; - -import { getClashInfo } from "./cmds"; - -let instancePromise: Promise = null!; - -async function getInstancePromise() { - let server = ""; - let secret = ""; - - try { - const info = await getClashInfo(); - - if (info?.server) { - server = info.server; - - // compatible width `external-controller` - if (server.startsWith(":")) server = `127.0.0.1${server}`; - else if (/^\d+$/.test(server)) server = `127.0.0.1:${server}`; - } - if (info?.secret) secret = info?.secret; - } catch {} - - const axiosIns = axios.create({ - baseURL: `http://${server}`, - headers: secret ? { Authorization: `Bearer ${secret}` } : {}, - timeout: 15000, - }); - axiosIns.interceptors.response.use((r) => r.data); - return axiosIns; -} - -/// initialize some information -/// enable force update axiosIns -export const getAxios = async (force: boolean = false) => { - if (!instancePromise || force) { - instancePromise = getInstancePromise(); - } - return instancePromise; -}; // Get current IP and geolocation information (refactored IP detection with service-specific mappings) interface IpInfo { @@ -213,6 +173,7 @@ export const getIpInfo = async (): Promise => { timeoutId = setTimeout(() => { timeoutController.abort(); }, service.timeout || serviceTimeout); + console.debug("Fetching IP information..."); const response = await fetch(service.url, { method: "GET", diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 724964051..99abd3fe6 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -18,6 +18,8 @@ export const translationKeys = [ "connections.components.order.uploadSpeed", "connections.components.order.downloadSpeed", "connections.components.actions.closeConnection", + "connections.components.columnManager.title", + "connections.components.columnManager.dragHandle", "home.page.tooltips.lightweightMode", "home.page.tooltips.manual", "home.page.tooltips.settings", @@ -460,6 +462,7 @@ export const translationKeys = [ "settings.modals.backup.actions.deleteBackup", "settings.modals.backup.actions.restore", "settings.modals.backup.actions.restoreBackup", + "settings.modals.backup.actions.viewHistory", "settings.modals.backup.fields.webdavUrl", "settings.modals.backup.fields.username", "settings.modals.backup.fields.info", @@ -476,8 +479,26 @@ export const translationKeys = [ "settings.modals.backup.messages.restoreSuccess", "settings.modals.backup.messages.localBackupExported", "settings.modals.backup.messages.localBackupExportFailed", + "settings.modals.backup.messages.webdavRefreshSuccess", + "settings.modals.backup.messages.webdavRefreshFailed", "settings.modals.backup.messages.confirmDelete", "settings.modals.backup.messages.confirmRestore", + "settings.modals.backup.auto.title", + "settings.modals.backup.auto.scheduleLabel", + "settings.modals.backup.auto.scheduleHelper", + "settings.modals.backup.auto.intervalLabel", + "settings.modals.backup.auto.changeLabel", + "settings.modals.backup.auto.changeHelper", + "settings.modals.backup.auto.options.hours", + "settings.modals.backup.auto.options.days", + "settings.modals.backup.manual.title", + "settings.modals.backup.manual.local", + "settings.modals.backup.manual.webdav", + "settings.modals.backup.manual.configureWebdav", + "settings.modals.backup.history.title", + "settings.modals.backup.history.summary", + "settings.modals.backup.history.empty", + "settings.modals.backup.webdav.title", "settings.modals.backup.table.filename", "settings.modals.backup.table.backupTime", "settings.modals.backup.table.actions", @@ -637,6 +658,8 @@ export const translationKeys = [ "shared.actions.resume", "shared.actions.closeAll", "shared.actions.clear", + "shared.actions.previous", + "shared.actions.next", "shared.labels.updateAt", "shared.labels.timeout", "shared.labels.icon", @@ -657,6 +680,7 @@ export const translationKeys = [ "shared.units.milliseconds", "shared.units.seconds", "shared.units.minutes", + "shared.units.hours", "shared.units.kilobytes", "shared.units.files", "shared.placeholders.filter", diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index fa34148b1..3c23476eb 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -8,6 +8,10 @@ export interface TranslationResources { actions: { closeConnection: string; }; + columnManager: { + dragHandle: string; + title: string; + }; fields: { chains: string; destination: string; @@ -686,12 +690,36 @@ export interface TranslationResources { restore: string; restoreBackup: string; selectTarget: string; + viewHistory: string; + }; + auto: { + changeHelper: string; + changeLabel: string; + intervalLabel: string; + options: { + days: string; + hours: string; + }; + scheduleHelper: string; + scheduleLabel: string; + title: string; }; fields: { info: string; username: string; webdavUrl: string; }; + history: { + empty: string; + summary: string; + title: string; + }; + manual: { + configureWebdav: string; + local: string; + title: string; + webdav: string; + }; messages: { backupCreated: string; backupFailed: string; @@ -707,6 +735,8 @@ export interface TranslationResources { usernameRequired: string; webdavConfigSaved: string; webdavConfigSaveFailed: string; + webdavRefreshFailed: string; + webdavRefreshSuccess: string; webdavUrlRequired: string; }; table: { @@ -721,6 +751,9 @@ export interface TranslationResources { webdav: string; }; title: string; + webdav: { + title: string; + }; }; clashCore: { variants: { @@ -1135,7 +1168,9 @@ export interface TranslationResources { hideDetails: string; listView: string; new: string; + next: string; pause: string; + previous: string; refresh: string; refreshPage: string; resetToDefault: string; @@ -1238,6 +1273,7 @@ export interface TranslationResources { }; units: { files: string; + hours: string; kilobytes: string; milliseconds: string; minutes: string; diff --git a/src/types/types.d.ts b/src/types/types.d.ts index cc9eaac37..317eaf5bf 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -854,6 +854,9 @@ interface IVergeConfig { enable_auto_delay_detection?: boolean; enable_builtin_enhanced?: boolean; auto_log_clean?: 0 | 1 | 2 | 3 | 4; + enable_auto_backup_schedule?: boolean; + auto_backup_interval_hours?: number; + auto_backup_on_change?: boolean; proxy_layout_column?: number; test_list?: IVergeTestItem[]; webdav_url?: string;