feat(notice): persist toast position preference (#5621)

* feat(notice): persist toast position preference

* docs: Changelog.md
This commit is contained in:
Sline
2025-12-19 18:04:23 +08:00
committed by GitHub
parent bd8eccdcea
commit fc84dc561c
21 changed files with 195 additions and 7 deletions

View File

@@ -9,6 +9,7 @@
- 允许代理页面允许高级过滤搜索
- 备份设置页面新增导入备份按钮
- 允许修改通知弹窗位置(「界面设置」->「通知位置」)
</details>

View File

@@ -66,6 +66,10 @@ pub struct IVerge {
#[serde(skip_serializing_if = "Option::is_none")]
pub menu_order: Option<Vec<String>>,
/// toast / notice position on screen
#[serde(skip_serializing_if = "Option::is_none")]
pub notice_position: Option<String>,
/// sysproxy tray icon
pub sysproxy_tray_icon: Option<bool>,
@@ -391,6 +395,7 @@ impl IVerge {
#[cfg(target_os = "macos")]
tray_icon: Some("monochrome".into()),
menu_icon: Some("monochrome".into()),
notice_position: Some("top-right".into()),
common_tray_icon: Some(false),
sysproxy_tray_icon: Some(false),
tun_tray_icon: Some(false),
@@ -475,6 +480,7 @@ impl IVerge {
patch!(tray_icon);
patch!(menu_icon);
patch!(menu_order);
patch!(notice_position);
patch!(common_tray_icon);
patch!(sysproxy_tray_icon);
patch!(tun_tray_icon);

View File

@@ -1,6 +1,12 @@
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import {
Snackbar,
Alert,
IconButton,
Box,
type SnackbarOrigin,
} from "@mui/material";
import React, { useMemo, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import {
@@ -10,8 +16,41 @@ import {
} from "@/services/notice-service";
import type { TranslationKey } from "@/types/generated/i18n-keys";
export const NoticeManager: React.FC = () => {
type NoticePosition = NonNullable<IVergeConfig["notice_position"]>;
const VALID_POSITIONS: NoticePosition[] = [
"top-left",
"top-right",
"bottom-left",
"bottom-right",
];
const resolvePosition = (position?: NoticePosition | null): NoticePosition => {
if (position && VALID_POSITIONS.includes(position)) {
return position;
}
return "top-right";
};
const getAnchorOrigin = (position: NoticePosition): SnackbarOrigin => {
const [vertical, horizontal] = position.split("-") as [
SnackbarOrigin["vertical"],
SnackbarOrigin["horizontal"],
];
return { vertical, horizontal };
};
interface NoticeManagerProps {
position?: NoticePosition | null;
}
export const NoticeManager: React.FC<NoticeManagerProps> = ({ position }) => {
const { t } = useTranslation();
const resolvedPosition = useMemo(() => resolvePosition(position), [position]);
const anchorOrigin = useMemo(
() => getAnchorOrigin(resolvedPosition),
[resolvedPosition],
);
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
@@ -25,8 +64,10 @@ export const NoticeManager: React.FC = () => {
<Box
sx={{
position: "fixed",
top: "20px",
right: "20px",
top: anchorOrigin.vertical === "top" ? "20px" : "auto",
bottom: anchorOrigin.vertical === "bottom" ? "20px" : "auto",
left: anchorOrigin.horizontal === "left" ? "20px" : "auto",
right: anchorOrigin.horizontal === "right" ? "20px" : "auto",
zIndex: 1500,
display: "flex",
flexDirection: "column",
@@ -38,7 +79,7 @@ export const NoticeManager: React.FC = () => {
<Snackbar
key={notice.id}
open={true}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
anchorOrigin={anchorOrigin}
sx={{
position: "relative",
transform: "none",

View File

@@ -190,6 +190,42 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</GuardState>
</Item>
<Item>
<ListItemText
primary={t("settings.components.verge.layout.fields.toastPosition")}
/>
<GuardState
value={verge?.notice_position ?? "top-right"}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(value) => onChangeData({ notice_position: value })}
onGuard={(value) => patchVerge({ notice_position: value })}
>
<Select size="small" sx={{ width: 180, "> div": { py: "7.5px" } }}>
<MenuItem value="top-right">
{t(
"settings.components.verge.layout.options.toastPosition.topRight",
)}
</MenuItem>
<MenuItem value="top-left">
{t(
"settings.components.verge.layout.options.toastPosition.topLeft",
)}
</MenuItem>
<MenuItem value="bottom-right">
{t(
"settings.components.verge.layout.options.toastPosition.bottomRight",
)}
</MenuItem>
<MenuItem value="bottom-left">
{t(
"settings.components.verge.layout.options.toastPosition.bottomLeft",
)}
</MenuItem>
</Select>
</GuardState>
</Item>
<Item>
<ListItemText
primary={

View File

@@ -205,6 +205,7 @@
"trafficGraph": "مخطط حركة المرور",
"memoryUsage": "استهلاك الذاكرة",
"proxyGroupIcon": "أيقونة مجموعة الوكلاء",
"toastPosition": "Toast Position",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "أيقونة التنقل",
@@ -225,6 +226,12 @@
"monochrome": "أحادي اللون",
"colorful": "ملون",
"disable": "تعطيل"
},
"toastPosition": {
"topLeft": "Top Left",
"topRight": "Top Right",
"bottomLeft": "Bottom Left",
"bottomRight": "Bottom Right"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Verkehrsdiagramm",
"memoryUsage": "Kern-Speichernutzung",
"proxyGroupIcon": "Proxy-Gruppen-Symbol",
"toastPosition": "Toast-Position",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Navigationsleiste-Symbol",
@@ -225,6 +226,12 @@
"monochrome": "Monochromes Symbol",
"colorful": "Farbiges Symbol",
"disable": "Deaktivieren"
},
"toastPosition": {
"topLeft": "Oben links",
"topRight": "Oben rechts",
"bottomLeft": "Unten links",
"bottomRight": "Unten rechts"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Traffic Graph",
"memoryUsage": "Core Usage",
"proxyGroupIcon": "Proxy Group Icon",
"toastPosition": "Toast Position",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Nav Icon",
@@ -225,6 +226,12 @@
"monochrome": "Monochrome",
"colorful": "Colorful",
"disable": "Disable"
},
"toastPosition": {
"topLeft": "Top Left",
"topRight": "Top Right",
"bottomLeft": "Bottom Left",
"bottomRight": "Bottom Right"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Gráfico de tráfico",
"memoryUsage": "Uso de memoria del núcleo",
"proxyGroupIcon": "Icono del grupo de proxy",
"toastPosition": "Posición del aviso",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Icono de la barra de navegación",
@@ -225,6 +226,12 @@
"monochrome": "Icono monocromo",
"colorful": "Icono colorido",
"disable": "Deshabilitar"
},
"toastPosition": {
"topLeft": "Arriba a la izquierda",
"topRight": "Arriba a la derecha",
"bottomLeft": "Abajo a la izquierda",
"bottomRight": "Abajo a la derecha"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "نمودار ترافیک",
"memoryUsage": "استفاده از حافظه",
"proxyGroupIcon": "آیکون گروه پراکسی",
"toastPosition": "Toast Position",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "آیکون ناوبری",
@@ -225,6 +226,12 @@
"monochrome": "تک رنگ",
"colorful": "رنگارنگ",
"disable": "غیرفعال کردن"
},
"toastPosition": {
"topLeft": "Top Left",
"topRight": "Top Right",
"bottomLeft": "Bottom Left",
"bottomRight": "Bottom Right"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Grafik Lalu Lintas",
"memoryUsage": "Penggunaan Memori",
"proxyGroupIcon": "Ikon Grup Proksi",
"toastPosition": "Posisi toast",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Ikon Navigasi",
@@ -225,6 +226,12 @@
"monochrome": "Monokrom",
"colorful": "Berwarna",
"disable": "Nonaktifkan"
},
"toastPosition": {
"topLeft": "Kiri atas",
"topRight": "Kanan atas",
"bottomLeft": "Kiri bawah",
"bottomRight": "Kanan bawah"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "トラフィックグラフ",
"memoryUsage": "コアメモリ使用量",
"proxyGroupIcon": "プロキシグループアイコン",
"toastPosition": "トーストの表示位置",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "ナビゲーションバーアイコン",
@@ -225,6 +226,12 @@
"monochrome": "モノクロアイコン",
"colorful": "カラーアイコン",
"disable": "無効にする"
},
"toastPosition": {
"topLeft": "左上",
"topRight": "右上",
"bottomLeft": "左下",
"bottomRight": "右下"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "트래픽 그래프",
"memoryUsage": "메모리 사용량",
"proxyGroupIcon": "프록시 그룹 아이콘",
"toastPosition": "토스트 위치",
"hoverNavigator": "호버 점프 내비게이터",
"hoverNavigatorDelay": "호버 점프 내비게이터 지연",
"navIcon": "내비게이션 아이콘",
@@ -225,6 +226,12 @@
"monochrome": "단색",
"colorful": "컬러",
"disable": "비활성화"
},
"toastPosition": {
"topLeft": "왼쪽 상단",
"topRight": "오른쪽 상단",
"bottomLeft": "왼쪽 하단",
"bottomRight": "오른쪽 하단"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "График трафика",
"memoryUsage": "Использование памяти",
"proxyGroupIcon": "Иконка Группы прокси",
"toastPosition": "Расположение уведомлений",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Иконки навигации",
@@ -225,6 +226,12 @@
"monochrome": "Монохромные",
"colorful": "Цветные",
"disable": "Отключить"
},
"toastPosition": {
"topLeft": "Сверху слева",
"topRight": "Сверху справа",
"bottomLeft": "Снизу слева",
"bottomRight": "Снизу справа"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Trafik Grafiği",
"memoryUsage": "Çekirdek Kullanımı",
"proxyGroupIcon": "Vekil Grup Simgesi",
"toastPosition": "Toast konumu",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Gezinme Simgesi",
@@ -225,6 +226,12 @@
"monochrome": "Tek Renkli",
"colorful": "Renkli",
"disable": "Devre Dışı Bırak"
},
"toastPosition": {
"topLeft": "Sol üst",
"topRight": "Sağ üst",
"bottomLeft": "Sol alt",
"bottomRight": "Sağ alt"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "Трафик графигы",
"memoryUsage": "Хәтер куллану",
"proxyGroupIcon": "Прокси төркеме иконкасы",
"toastPosition": "Toast Position",
"hoverNavigator": "Hover Jump Navigator",
"hoverNavigatorDelay": "Hover Jump Navigator Delay",
"navIcon": "Навигация иконкасы",
@@ -225,6 +226,12 @@
"monochrome": "Монохром",
"colorful": "Төсле",
"disable": "Сүндерү"
},
"toastPosition": {
"topLeft": "Top Left",
"topRight": "Top Right",
"bottomLeft": "Bottom Left",
"bottomRight": "Bottom Right"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "流量图显",
"memoryUsage": "内核占用",
"proxyGroupIcon": "代理组图标",
"toastPosition": "通知位置",
"hoverNavigator": "悬浮跳转导航",
"hoverNavigatorDelay": "悬浮跳转导航延迟",
"navIcon": "导航栏图标",
@@ -225,6 +226,12 @@
"monochrome": "单色图标",
"colorful": "彩色图标",
"disable": "禁用"
},
"toastPosition": {
"topLeft": "左上角",
"topRight": "右上角",
"bottomLeft": "左下角",
"bottomRight": "右下角"
}
}
}

View File

@@ -205,6 +205,7 @@
"trafficGraph": "流量圖表",
"memoryUsage": "內核佔用",
"proxyGroupIcon": "代理組圖示",
"toastPosition": "通知位置",
"hoverNavigator": "懸浮跳轉導航",
"hoverNavigatorDelay": "懸浮跳轉導航延遲",
"navIcon": "導覽列圖示",
@@ -225,6 +226,12 @@
"monochrome": "單色圖示",
"colorful": "彩色圖示",
"disable": "停用"
},
"toastPosition": {
"topLeft": "左上角",
"topRight": "右上角",
"bottomLeft": "左下角",
"bottomRight": "右下角"
}
}
}

View File

@@ -263,7 +263,7 @@ const Layout = () => {
>
<ThemeProvider theme={theme}>
{/* 左侧底部窗口控制按钮 */}
<NoticeManager />
<NoticeManager position={verge?.notice_position} />
<div
style={{
animation: "fadeIn 0.5s",

View File

@@ -425,6 +425,7 @@ export const translationKeys = [
"settings.components.verge.layout.fields.trafficGraph",
"settings.components.verge.layout.fields.memoryUsage",
"settings.components.verge.layout.fields.proxyGroupIcon",
"settings.components.verge.layout.fields.toastPosition",
"settings.components.verge.layout.fields.hoverNavigator",
"settings.components.verge.layout.fields.hoverNavigatorDelay",
"settings.components.verge.layout.fields.navIcon",
@@ -440,6 +441,10 @@ export const translationKeys = [
"settings.components.verge.layout.options.icon.monochrome",
"settings.components.verge.layout.options.icon.colorful",
"settings.components.verge.layout.options.icon.disable",
"settings.components.verge.layout.options.toastPosition.topLeft",
"settings.components.verge.layout.options.toastPosition.topRight",
"settings.components.verge.layout.options.toastPosition.bottomLeft",
"settings.components.verge.layout.options.toastPosition.bottomRight",
"settings.modals.clashPort.title",
"settings.modals.clashPort.fields.mixed",
"settings.modals.clashPort.fields.socks",

View File

@@ -627,6 +627,7 @@ export interface TranslationResources {
proxyGroupIcon: string;
showProxyGroupsInline: string;
systemProxyTrayIcon: string;
toastPosition: string;
trafficGraph: string;
trayIcon: string;
tunTrayIcon: string;
@@ -637,6 +638,12 @@ export interface TranslationResources {
disable: string;
monochrome: string;
};
toastPosition: {
bottomLeft: string;
bottomRight: string;
topLeft: string;
topRight: string;
};
};
title: string;
tooltips: {

View File

@@ -814,6 +814,7 @@ interface IVergeConfig {
enable_group_icon?: boolean;
menu_icon?: "monochrome" | "colorful" | "disable";
menu_order?: string[];
notice_position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
tray_icon?: "monochrome" | "colorful";
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;