mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat: add notification system with hotkey events and permission handling (#3867)
* feat: add notification system with hotkey events and permission handling * Add macOS-specific handling for AppHidden notification Introduces conditional support for the AppHidden notification event, enabling macOS-specific behavior. Updates the enum and notification logic to include this platform-specific feature. Improves macOS user experience by accommodating system-level application hiding events. * Implement feature X to enhance user experience and fix bug Y in module Z * refactor(notification): update notification keys for consistency and clarity * chore(deps): update dependencies to latest versions
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
|
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
|
||||||
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
|
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
|
||||||
|
- 快捷键事件通过系统通知
|
||||||
|
|
||||||
### 🚀 优化改进
|
### 🚀 优化改进
|
||||||
|
|
||||||
|
|||||||
1729
src-tauri/Cargo.lock
generated
1729
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ hmac = "0.12.1"
|
|||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"autostart:allow-enable",
|
"autostart:allow-enable",
|
||||||
"autostart:allow-disable",
|
"autostart:allow-disable",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:allow-is-enabled",
|
||||||
"core:window:allow-set-theme"
|
"core:window:allow-set-theme",
|
||||||
|
"notification:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::utils::notification::{notify_event, NotificationEvent};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config, core::handle, feat, logging, logging_error,
|
config::Config, core::handle, feat, logging, logging_error,
|
||||||
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
module::lightweight::entry_lightweight_mode, utils::logging::Type,
|
||||||
@@ -133,14 +134,11 @@ impl Hotkey {
|
|||||||
manager.unregister(hotkey)?;
|
manager.unregister(hotkey)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = match func.trim() {
|
let app_handle_clone = app_handle.clone();
|
||||||
|
let f: Box<dyn Fn() + Send + Sync> = match func.trim() {
|
||||||
"open_or_close_dashboard" => {
|
"open_or_close_dashboard" => {
|
||||||
logging!(
|
let app_handle = app_handle_clone.clone();
|
||||||
debug,
|
Box::new(move || {
|
||||||
Type::Hotkey,
|
|
||||||
"Registering open_or_close_dashboard function"
|
|
||||||
);
|
|
||||||
|| {
|
|
||||||
logging!(
|
logging!(
|
||||||
debug,
|
debug,
|
||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
@@ -162,18 +160,75 @@ impl Hotkey {
|
|||||||
Type::Hotkey,
|
Type::Hotkey,
|
||||||
"=== Hotkey Dashboard Window Operation End ==="
|
"=== Hotkey Dashboard Window Operation End ==="
|
||||||
);
|
);
|
||||||
|
notify_event(&app_handle, NotificationEvent::DashboardToggled);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
"clash_mode_rule" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("rule".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Rule" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"clash_mode_global" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("global".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Global" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"clash_mode_direct" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::change_clash_mode("direct".into());
|
||||||
|
notify_event(
|
||||||
|
&app_handle,
|
||||||
|
NotificationEvent::ClashModeChanged { mode: "Direct" },
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"toggle_system_proxy" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::toggle_system_proxy();
|
||||||
|
notify_event(&app_handle, NotificationEvent::SystemProxyToggled);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"toggle_tun_mode" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::toggle_tun_mode(None);
|
||||||
|
notify_event(&app_handle, NotificationEvent::TunModeToggled);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"entry_lightweight_mode" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
entry_lightweight_mode();
|
||||||
|
notify_event(&app_handle, NotificationEvent::LightweightModeEntered);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::quit();
|
||||||
|
notify_event(&app_handle, NotificationEvent::AppQuit);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
|
||||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
|
||||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
|
||||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
|
||||||
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
|
|
||||||
"entry_lightweight_mode" => || entry_lightweight_mode(),
|
|
||||||
"quit" => || feat::quit(),
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
"hide" => || feat::hide(),
|
"hide" => {
|
||||||
|
let app_handle = app_handle_clone.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
feat::hide();
|
||||||
|
notify_event(&app_handle, NotificationEvent::AppHidden);
|
||||||
|
})
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
logging!(error, Type::Hotkey, "Invalid function: {}", func);
|
||||||
bail!("invalid function \"{func}\"");
|
bail!("invalid function \"{func}\"");
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ pub fn run() {
|
|||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod i18n;
|
|||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod notification;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod tmpl;
|
pub mod tmpl;
|
||||||
|
|||||||
70
src-tauri/src/utils/notification.rs
Normal file
70
src-tauri/src/utils/notification.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
|
pub enum NotificationEvent<'a> {
|
||||||
|
DashboardToggled,
|
||||||
|
ClashModeChanged {
|
||||||
|
mode: &'a str,
|
||||||
|
},
|
||||||
|
SystemProxyToggled,
|
||||||
|
TunModeToggled,
|
||||||
|
LightweightModeEntered,
|
||||||
|
AppQuit,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
AppHidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||||
|
app.notification()
|
||||||
|
.builder()
|
||||||
|
.title(title)
|
||||||
|
.body(body)
|
||||||
|
.show()
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_event(app: &AppHandle, event: NotificationEvent) {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
match event {
|
||||||
|
NotificationEvent::DashboardToggled => {
|
||||||
|
notify(app, &t("DashboardToggledTitle"), &t("DashboardToggledBody"));
|
||||||
|
}
|
||||||
|
NotificationEvent::ClashModeChanged { mode } => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("ClashModeChangedTitle"),
|
||||||
|
&t_with_args("ClashModeChangedBody", mode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::SystemProxyToggled => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("SystemProxyToggledTitle"),
|
||||||
|
&t("SystemProxyToggledBody"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::TunModeToggled => {
|
||||||
|
notify(app, &t("TunModeToggledTitle"), &t("TunModeToggledBody"));
|
||||||
|
}
|
||||||
|
NotificationEvent::LightweightModeEntered => {
|
||||||
|
notify(
|
||||||
|
app,
|
||||||
|
&t("LightweightModeEnteredTitle"),
|
||||||
|
&t("LightweightModeEnteredBody"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NotificationEvent::AppQuit => {
|
||||||
|
notify(app, &t("AppQuitTitle"), &t("AppQuitBody"));
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
NotificationEvent::AppHidden => {
|
||||||
|
notify(app, &t("AppHiddenTitle"), &t("AppHiddenBody"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数,带参数的i18n
|
||||||
|
fn t_with_args(key: &str, mode: &str) -> String {
|
||||||
|
use crate::utils::i18n::t;
|
||||||
|
t(key).replace("{mode}", mode)
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { AppDataProvider } from "./providers/app-data-provider";
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
import Layout from "./pages/_layout";
|
import Layout from "./pages/_layout";
|
||||||
|
import { useNotificationPermission } from "./hooks/useNotificationPermission";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useNotificationPermission();
|
||||||
return (
|
return (
|
||||||
<AppDataProvider>
|
<AppDataProvider>
|
||||||
<Layout />
|
<Layout />
|
||||||
|
|||||||
8
src/hooks/useNotificationPermission.ts
Normal file
8
src/hooks/useNotificationPermission.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { setupNotificationPermission } from "../utils/notification-permission";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function useNotificationPermission() {
|
||||||
|
useEffect(() => {
|
||||||
|
setupNotificationPermission();
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -624,5 +624,19 @@
|
|||||||
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
||||||
"Unsupported Country/Region": "Unsupported Country/Region",
|
"Unsupported Country/Region": "Unsupported Country/Region",
|
||||||
"Failed (Network Connection)": "Failed (Network Connection)",
|
"Failed (Network Connection)": "Failed (Network Connection)",
|
||||||
|
"DashboardToggledTitle": "Dashboard Toggled",
|
||||||
|
"DashboardToggledBody": "Dashboard visibility toggled by hotkey",
|
||||||
|
"ClashModeChangedTitle": "Clash Mode Changed",
|
||||||
|
"ClashModeChangedBody": "Switched to {mode} mode",
|
||||||
|
"SystemProxyToggledTitle": "System Proxy Toggled",
|
||||||
|
"SystemProxyToggledBody": "System proxy state toggled by hotkey",
|
||||||
|
"TunModeToggledTitle": "TUN Mode Toggled",
|
||||||
|
"TunModeToggledBody": "TUN mode toggled by hotkey",
|
||||||
|
"LightweightModeEnteredTitle": "Lightweight Mode",
|
||||||
|
"LightweightModeEnteredBody": "Entered lightweight mode by hotkey",
|
||||||
|
"AppQuitTitle": "APP Quit",
|
||||||
|
"AppQuitBody": "APP quit by hotkey",
|
||||||
|
"AppHiddenTitle": "APP Hidden",
|
||||||
|
"AppHiddenBody": "APP window hidden by hotkey",
|
||||||
"Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://"
|
"Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,5 +624,19 @@
|
|||||||
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
||||||
"Unsupported Country/Region": "不支持的国家/地区",
|
"Unsupported Country/Region": "不支持的国家/地区",
|
||||||
"Failed (Network Connection)": "测试失败(网络连接问题)",
|
"Failed (Network Connection)": "测试失败(网络连接问题)",
|
||||||
|
"DashboardToggledTitle": "仪表盘已切换",
|
||||||
|
"DashboardToggledBody": "已通过快捷键切换仪表盘显示状态",
|
||||||
|
"ClashModeChangedTitle": "Clash 模式切换",
|
||||||
|
"ClashModeChangedBody": "已切换为 {mode} 模式",
|
||||||
|
"SystemProxyToggledTitle": "系统代理切换",
|
||||||
|
"SystemProxyToggledBody": "已通过快捷键切换系统代理状态",
|
||||||
|
"TunModeToggledTitle": "TUN 模式切换",
|
||||||
|
"TunModeToggledBody": "已通过快捷键切换 TUN 模式",
|
||||||
|
"LightweightModeEnteredTitle": "轻量模式",
|
||||||
|
"LightweightModeEnteredBody": "已通过快捷键进入轻量模式",
|
||||||
|
"AppQuitTitle": "应用退出",
|
||||||
|
"AppQuitBody": "已通过快捷键退出应用",
|
||||||
|
"AppHiddenTitle": "应用隐藏",
|
||||||
|
"AppHiddenBody": "已通过快捷键隐藏应用窗口",
|
||||||
"Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址"
|
"Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址"
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/utils/notification-permission.ts
Normal file
17
src/utils/notification-permission.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
} from "@tauri-apps/plugin-notification";
|
||||||
|
|
||||||
|
export async function setupNotificationPermission() {
|
||||||
|
let permission = await isPermissionGranted();
|
||||||
|
if (!permission) {
|
||||||
|
const result = await requestPermission();
|
||||||
|
permission = result === "granted";
|
||||||
|
}
|
||||||
|
if (permission) {
|
||||||
|
console.log("通知权限已授予");
|
||||||
|
} else {
|
||||||
|
console.log("通知权限被拒绝");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user