From a0b5867c9cf18bd83c1542307834a665c6f19a54 Mon Sep 17 00:00:00 2001 From: ayangweb <75017711+ayangweb@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:42:55 +0800 Subject: [PATCH] feat: tray internationalization and shortcut display (#259) --- package.json | 1 + pnpm-lock.yaml | 10 +++ src-tauri/capabilities/default.json | 3 +- src-tauri/src/lib.rs | 17 +++- src/components/Settings/GeneralSettings.tsx | 8 ++ src/hooks/useTray.ts | 88 +++++++++++++++++++++ src/locales/en/translation.json | 5 ++ src/locales/zh/translation.json | 5 ++ src/pages/main/index.tsx | 5 +- src/pages/settings/index.tsx | 17 ++-- src/stores/appStore.ts | 37 +++++---- 11 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useTray.ts diff --git a/package.json b/package.json index 04816735..3d87591c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tauri-apps/plugin-global-shortcut": "~2.0.0", "@tauri-apps/plugin-http": "~2.0.2", "@tauri-apps/plugin-os": "^2.2.0", + "@tauri-apps/plugin-process": "^2.2.0", "@tauri-apps/plugin-shell": "^2.2.0", "@tauri-apps/plugin-updater": "^2.5.1", "@tauri-apps/plugin-websocket": "~2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5905cd0f..dd033fa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@tauri-apps/plugin-os': specifier: ^2.2.0 version: 2.2.0 + '@tauri-apps/plugin-process': + specifier: ^2.2.0 + version: 2.2.0 '@tauri-apps/plugin-shell': specifier: ^2.2.0 version: 2.2.0 @@ -1098,6 +1101,9 @@ packages: '@tauri-apps/plugin-os@2.2.0': resolution: {integrity: sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==} + '@tauri-apps/plugin-process@2.2.0': + resolution: {integrity: sha512-uypN2Crmyop9z+KRJr3zl71OyVFgTuvHFjsJ0UxxQ/J5212jVa5w4nPEYjIewcn8bUEXacRebwE6F7owgrbhSw==} + '@tauri-apps/plugin-shell@2.2.0': resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==} @@ -4138,6 +4144,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.3.0 + '@tauri-apps/plugin-process@2.2.0': + dependencies: + '@tauri-apps/api': 2.3.0 + '@tauri-apps/plugin-shell@2.2.0': dependencies: '@tauri-apps/api': 2.3.0 diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f8fd83bc..09b5e02f 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -66,6 +66,7 @@ "fs-pro:default", "macos-permissions:default", "screenshots:default", - "core:window:allow-set-theme" + "core:window:allow-set-theme", + "process:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b1c25b47..2305afcb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -82,7 +82,8 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs_pro::init()) .plugin(tauri_plugin_macos_permissions::init()) - .plugin(tauri_plugin_screenshots::init()); + .plugin(tauri_plugin_screenshots::init()) + .plugin(tauri_plugin_process::init()); // Conditional compilation for macOS #[cfg(target_os = "macos")] @@ -97,7 +98,9 @@ pub fn run() { shortcut::unregister_shortcut, shortcut::get_current_shortcut, change_autostart, + show_coco, hide_coco, + show_settings, server::servers::get_server_token, server::servers::add_coco_server, server::servers::remove_coco_server, @@ -143,7 +146,7 @@ pub fn run() { }); shortcut::enable_shortcut(&app); - enable_tray(app); + // enable_tray(app); enable_autostart(app); #[cfg(target_os = "macos")] @@ -239,6 +242,11 @@ async fn init_app_search_source(app_handle: &AppHandle) -> Result Ok(()) } +#[tauri::command] +async fn show_coco(app_handle: AppHandle) { + handle_open_coco(&app_handle); +} + #[tauri::command] fn hide_coco(app: tauri::AppHandle) { if let Some(window) = app.get_window(MAIN_WINDOW_LABEL) { @@ -475,3 +483,8 @@ async fn get_app_search_source(app_handle: AppHandle) -> Result<( Ok(()) } + +#[tauri::command] +async fn show_settings(app_handle: AppHandle) { + open_settings(&app_handle); +} diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index 1b680350..73b23f8e 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -113,6 +113,14 @@ export default function GeneralSettings() { const [shortcut, setShortcut] = useState([]); + const setShowCocoShortcuts = useAppStore((state) => { + return state.setShowCocoShortcuts; + }); + + useEffect(() => { + setShowCocoShortcuts(shortcut); + }, [shortcut]); + async function getCurrentShortcut() { try { const res: any = await invoke("get_current_shortcut"); diff --git a/src/hooks/useTray.ts b/src/hooks/useTray.ts new file mode 100644 index 00000000..f45d0eb3 --- /dev/null +++ b/src/hooks/useTray.ts @@ -0,0 +1,88 @@ +import { useTranslation } from "react-i18next"; +import { TrayIcon, type TrayIconOptions } from "@tauri-apps/api/tray"; +import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; +import { isMac } from "@/utils/platform"; +import { resolveResource } from "@tauri-apps/api/path"; +import { useUpdateEffect } from "ahooks"; +import { exit } from "@tauri-apps/plugin-process"; +import { invoke } from "@tauri-apps/api/core"; +import { useAppStore } from "@/stores/appStore"; + +const TRAY_ID = "COCO_TRAY"; + +export const useTray = () => { + const { t, i18n } = useTranslation(); + const showCocoShortcuts = useAppStore((state) => state.showCocoShortcuts); + + useUpdateEffect(() => { + if (showCocoShortcuts.length === 0) return; + + updateTrayMenu(); + }, [i18n.language, showCocoShortcuts]); + + const getTrayById = () => { + return TrayIcon.getById(TRAY_ID); + }; + + const createTrayIcon = async () => { + const tray = await getTrayById(); + + if (tray) return; + + const menu = await getTrayMenu(); + + const iconPath = isMac ? "assets/tray-mac.ico" : "assets/tray.ico"; + const icon = await resolveResource(iconPath); + + const options: TrayIconOptions = { + menu, + icon, + id: TRAY_ID, + iconAsTemplate: true, + }; + + return TrayIcon.new(options); + }; + + const getTrayMenu = async () => { + const items = await Promise.all([ + MenuItem.new({ + text: t("tray.showCoco"), + accelerator: showCocoShortcuts.join("+"), + action: () => { + invoke("show_coco"); + }, + }), + PredefinedMenuItem.new({ item: "Separator" }), + MenuItem.new({ + text: t("tray.settings"), + // accelerator: "CommandOrControl+,", + action: () => { + invoke("show_settings"); + }, + }), + PredefinedMenuItem.new({ item: "Separator" }), + MenuItem.new({ + text: t("tray.quitCoco"), + accelerator: "CommandOrControl+Q", + action: () => { + exit(0); + }, + }), + ]); + + return Menu.new({ items }); + }; + + const updateTrayMenu = async () => { + const tray = await getTrayById(); + + if (!tray) { + return createTrayIcon(); + } + + const menu = await getTrayMenu(); + + tray.setMenu(menu); + }; +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9d290bb8..66ba603d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -201,5 +201,10 @@ }, "enable_server": "Enable Server", "disable_server": "Disable Server" + }, + "tray": { + "showCoco": "Show Coco", + "settings": "Settings...", + "quitCoco": "Quit Coco" } } diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index abb91ab6..53070081 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -201,5 +201,10 @@ }, "enable_server": "启用服务", "disable_server": "禁用服务" + }, + "tray": { + "showCoco": "显示 Coco", + "settings": "偏好设置", + "quitCoco": "退出 Coco" } } diff --git a/src/pages/main/index.tsx b/src/pages/main/index.tsx index f6e21e52..9bc5b765 100644 --- a/src/pages/main/index.tsx +++ b/src/pages/main/index.tsx @@ -10,7 +10,6 @@ import ChatAI, { ChatAIRef } from "@/components/Assistant/Chat"; import { useAppStore } from "@/stores/appStore"; import { useAuthStore } from "@/stores/authStore"; import { isWin } from "@/utils/platform"; -import { useMount } from "ahooks"; export default function DesktopApp() { const initializeListeners = useAppStore((state) => state.initializeListeners); @@ -20,9 +19,9 @@ export default function DesktopApp() { const isPinned = useAppStore((state) => state.isPinned); - useMount(() => { + useEffect(() => { invoke("get_app_search_source"); - }); + }, []); useEffect(() => { initializeListeners(); diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index 4c9f8a99..99783fda 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -2,25 +2,28 @@ import { useState, useEffect } from "react"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; import { Settings, Puzzle, Settings2, Info, Server } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { listen } from '@tauri-apps/api/event'; +import { listen } from "@tauri-apps/api/event"; import SettingsPanel from "@/components/Settings/SettingsPanel"; import GeneralSettings from "@/components/Settings/GeneralSettings"; import AboutView from "@/components/Settings/AboutView"; import Cloud from "@/components/Cloud/Cloud.tsx"; import Footer from "@/components/Footer"; +import { useTray } from "@/hooks/useTray"; const tabIndexMap: { [key: string]: number } = { - 'general': 0, - 'extensions': 1, - 'connect': 2, - 'advanced': 3, - 'about': 4 + general: 0, + extensions: 1, + connect: 2, + advanced: 3, + about: 4, }; function SettingsPage() { const { t } = useTranslation(); + useTray(); + const tabs = [ { name: t("settings.tabs.general"), icon: Settings }, { name: t("settings.tabs.extensions"), icon: Puzzle }, @@ -41,7 +44,7 @@ function SettingsPage() { }); return () => { - unlisten.then(fn => fn()); + unlisten.then((fn) => fn()); }; }, []); diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 677fac8c..aa55ca12 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -1,10 +1,10 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import { listen, emit } from '@tauri-apps/api/event'; +import { listen, emit } from "@tauri-apps/api/event"; -import { AppEndpoint } from "@/utils/tauri" +import { AppEndpoint } from "@/utils/tauri"; -const ENDPOINT_CHANGE_EVENT = 'endpoint-changed'; +const ENDPOINT_CHANGE_EVENT = "endpoint-changed"; export interface IServer { id: string; @@ -19,7 +19,7 @@ export interface IServer { available?: boolean; health?: { status: string; - }, + }; assistantCount?: number; } @@ -28,23 +28,26 @@ export type IAppStore = { setShowTooltip: (showTooltip: boolean) => void; error: string; - setError: (message: any) => void, + setError: (message: any) => void; ssoRequestID: string; - setSSORequestID: (ssoRequestID: string) => void, + setSSORequestID: (ssoRequestID: string) => void; // ssoServerID: string; // setSSOServerID: (ssoServerID: string) => void, - endpoint: AppEndpoint, - endpoint_http: string, - endpoint_websocket: string, - setEndpoint: (endpoint: AppEndpoint) => void, + endpoint: AppEndpoint; + endpoint_http: string; + endpoint_websocket: string; + setEndpoint: (endpoint: AppEndpoint) => void; language: string; setLanguage: (language: string) => void; - isPinned: boolean, - setIsPinned: (isPinned: boolean) => void, + isPinned: boolean; + setIsPinned: (isPinned: boolean) => void; initializeListeners: () => void; + + showCocoShortcuts: string[]; + setShowCocoShortcuts: (showCocoShortcuts: string[]) => void; }; export const useAppStore = create()( @@ -66,7 +69,7 @@ export const useAppStore = create()( const withoutProtocol = endpoint.split("//")[1]; - const endpoint_websocket = endpoint?.includes('https') + const endpoint_websocket = endpoint?.includes("https") ? `wss://${withoutProtocol}/ws` : `ws://${withoutProtocol}/ws`; @@ -79,7 +82,7 @@ export const useAppStore = create()( await emit(ENDPOINT_CHANGE_EVENT, { endpoint, endpoint_http, - endpoint_websocket + endpoint_websocket, }); }, language: "en", @@ -92,6 +95,12 @@ export const useAppStore = create()( set({ endpoint, endpoint_http, endpoint_websocket }); }); }, + showCocoShortcuts: [], + setShowCocoShortcuts: (showCocoShortcuts: string[]) => { + console.log("set showCocoShortcuts", showCocoShortcuts); + + return set({ showCocoShortcuts }); + }, }), { name: "app-store",