diff --git a/src/App.tsx b/src/App.tsx index 0328f3f8..e2376c4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,11 @@ const App = () => { // 生成 antd 的颜色变量 generateColorVars(); + + // 若未完成引导,跳转到引导页 + if (!globalStore.app.hasCompletedOnboarding) { + router.navigate("/onboarding"); + } }); // 监听语言的变化 diff --git a/src/constants/index.ts b/src/constants/index.ts index 59dec314..85d2c6f4 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -31,6 +31,7 @@ export const LISTEN_KEY = { CLIPBOARD_ITEM_SELECT_NEXT: "clipboard-item-select-next", CLIPBOARD_ITEM_SELECT_PREV: "clipboard-item-select-prev", CLOSE_DATABASE: "close-database", + PREFERENCE_NAVIGATE: "preference-navigate", REFRESH_CLIPBOARD_LIST: "refresh-clipboard-list", SEND_MODAL_DATA: "send-modal-data", SEND_MODAL_SEND: "send-modal-send", diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 065281a8..1296d62d 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -160,6 +160,41 @@ "context_menu": { "set_as_expansion": "Set as Text Expansion" }, + "onboarding": { + "complete": { + "button": "Complete", + "hint": "Please complete all permission authorizations first" + }, + "permission": { + "accessibility": { + "description": "Used for auto-paste and global shortcut monitoring", + "title": "Accessibility" + }, + "authorize": "Authorize", + "authorized": "Authorized", + "full_disk_access": { + "description": "Used for reading files in protected directories", + "title": "Full Disk Access" + }, + "screen_recording": { + "description": "Used for screenshot functionality", + "title": "Screen Recording" + } + }, + "permissions": { + "description": "WeCut requires the following permissions to work properly. Please complete each authorization.", + "title": "macOS Permissions" + }, + "privacy": { + "description": "WeCut is a fully open-source project. All data is stored locally on your device only. We do not collect any user data or personal information.", + "view_source": "View Source Code" + }, + "wegent": { + "description": "After configuring Wegent Key, you can use AI clipboard assistant, work queue and other enhanced features. You can configure it later in Preferences.", + "goto_preference": "Go to Preferences", + "title": "Configure Wegent Key (Optional)" + } + }, "preference": { "about": { "about_software": { diff --git a/src/locales/ja-JP.json b/src/locales/ja-JP.json index ef4346a0..fb75a361 100644 --- a/src/locales/ja-JP.json +++ b/src/locales/ja-JP.json @@ -100,6 +100,41 @@ } } }, + "onboarding": { + "complete": { + "button": "完了", + "hint": "すべての権限を先に承認してください" + }, + "permission": { + "accessibility": { + "description": "自動貼り付けとグローバルショートカットの監視に使用", + "title": "アクセシビリティ" + }, + "authorize": "承認する", + "authorized": "承認済み", + "full_disk_access": { + "description": "保護されたパスのファイル読み取りに使用", + "title": "フルディスクアクセス" + }, + "screen_recording": { + "description": "スクリーンショット機能に使用", + "title": "画面収録" + } + }, + "permissions": { + "description": "WeCut が正常に動作するには以下の権限が必要です。各権限を承認してください。", + "title": "macOS 権限の承認" + }, + "privacy": { + "description": "WeCut は完全なオープンソースプロジェクトです。すべてのデータはお使いのデバイスにのみローカルに保存されます。ユーザーデータや個人情報は一切収集しません。", + "view_source": "ソースコードを見る" + }, + "wegent": { + "description": "Wegent Key を設定すると、AI クリップボードアシスタントやワークキューなどの拡張機能が使用できます。後で環境設定で設定することもできます。", + "goto_preference": "環境設定に移動", + "title": "Wegent Key の設定(任意)" + } + }, "preference": { "about": { "about_software": { diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 86cb2b99..4c49fa22 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -160,6 +160,41 @@ "context_menu": { "set_as_expansion": "设为快捷粘贴" }, + "onboarding": { + "complete": { + "button": "完成", + "hint": "请先完成所有权限授权" + }, + "permission": { + "accessibility": { + "description": "用于自动粘贴、监听全局快捷键", + "title": "辅助功能(Accessibility)" + }, + "authorize": "去授权", + "authorized": "已授权", + "full_disk_access": { + "description": "用于读取受保护路径下的文件内容", + "title": "完全磁盘访问(Full Disk Access)" + }, + "screen_recording": { + "description": "用于截图功能", + "title": "屏幕录制(Screen Recording)" + } + }, + "permissions": { + "description": "WeCut 需要以下权限才能正常运行,请逐一完成授权。", + "title": "macOS 权限授权" + }, + "privacy": { + "description": "WeCut 是一个完全开源的项目,所有数据仅存储在您本地设备上,我们不会收集任何用户数据或隐私信息。", + "view_source": "查看源代码" + }, + "wegent": { + "description": "配置 Wegent Key 后,可使用 AI 剪切板助手、工作队列等增强功能。可稍后在偏好设置中配置。", + "goto_preference": "前往偏好设置配置", + "title": "配置 Wegent Key(可选)" + } + }, "preference": { "about": { "about_software": { diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 6fc3ad34..0231805e 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -100,6 +100,41 @@ } } }, + "onboarding": { + "complete": { + "button": "完成", + "hint": "請先完成所有權限授權" + }, + "permission": { + "accessibility": { + "description": "用於自動貼上、監聽全域快捷鍵", + "title": "輔助使用功能(Accessibility)" + }, + "authorize": "去授權", + "authorized": "已授權", + "full_disk_access": { + "description": "用於讀取受保護路徑下的檔案內容", + "title": "完整磁碟存取(Full Disk Access)" + }, + "screen_recording": { + "description": "用於截圖功能", + "title": "螢幕錄製(Screen Recording)" + } + }, + "permissions": { + "description": "WeCut 需要以下權限才能正常運作,請逐一完成授權。", + "title": "macOS 權限授權" + }, + "privacy": { + "description": "WeCut 是一個完全開源的專案,所有資料僅存儲在您的本機裝置上,我們不會收集任何使用者資料或隱私資訊。", + "view_source": "查看原始碼" + }, + "wegent": { + "description": "設定 Wegent Key 後,可使用 AI 剪貼簿助手、工作佇列等增強功能。可稍後在偏好設定中設定。", + "goto_preference": "前往偏好設定", + "title": "設定 Wegent Key(可選)" + } + }, "preference": { "about": { "about_software": { diff --git a/src/pages/Onboarding/index.tsx b/src/pages/Onboarding/index.tsx new file mode 100644 index 00000000..1e86f8ea --- /dev/null +++ b/src/pages/Onboarding/index.tsx @@ -0,0 +1,262 @@ +import { emit } from "@tauri-apps/api/event"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { useInterval } from "ahooks"; +import { Button, Card, Flex, Tooltip, Typography } from "antd"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { + checkAccessibilityPermission, + checkFullDiskAccessPermission, + checkScreenRecordingPermission, + requestAccessibilityPermission, + requestFullDiskAccessPermission, + requestScreenRecordingPermission, +} from "tauri-plugin-macos-permissions-api"; +import UnoIcon from "@/components/UnoIcon"; +import { LISTEN_KEY, WINDOW_LABEL } from "@/constants"; +import { showWindow } from "@/plugins/window"; +import { globalStore } from "@/stores/global"; +import { saveStore } from "@/utils/store"; + +const { Title, Text, Paragraph } = Typography; + +const GITHUB_URL = "https://github.com/wecode-ai/WeCut"; + +const Onboarding = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [accessibilityGranted, setAccessibilityGranted] = useState(false); + const [screenRecordingGranted, setScreenRecordingGranted] = useState(false); + const [fullDiskAccessGranted, setFullDiskAccessGranted] = useState(false); + + const allGranted = + accessibilityGranted && screenRecordingGranted && fullDiskAccessGranted; + + // 初始检测权限状态 + useEffect(() => { + checkAllPermissions(); + }, []); + + // 每秒轮询权限状态(用户从系统设置返回后立即刷新) + useInterval(() => { + checkAllPermissions(); + }, 1000); + + const checkAllPermissions = async () => { + const [accessibility, screenRecording, fullDisk] = await Promise.all([ + checkAccessibilityPermission(), + checkScreenRecordingPermission(), + checkFullDiskAccessPermission(), + ]); + setAccessibilityGranted(accessibility); + setScreenRecordingGranted(screenRecording); + setFullDiskAccessGranted(fullDisk); + }; + + const handleRequestAccessibility = async () => { + await requestAccessibilityPermission(); + }; + + const handleRequestScreenRecording = async () => { + await requestScreenRecordingPermission(); + }; + + const handleRequestFullDiskAccess = async () => { + await requestFullDiskAccessPermission(); + }; + + const handleComplete = async () => { + globalStore.app.hasCompletedOnboarding = true; + await saveStore(); + navigate("/"); + }; + + const handleOpenPreference = async () => { + globalStore.app.hasCompletedOnboarding = true; + await saveStore(); + navigate("/"); + // 通知偏好设置窗口切换到 Wegent 集成 tab + await showWindow(WINDOW_LABEL.PREFERENCE as any); + await emit(LISTEN_KEY.PREFERENCE_NAVIGATE, "wegent"); + }; + + const renderPermissionStatus = ( + granted: boolean, + onRequest: () => void, + ) => { + if (granted) { + return ( + + + {t("onboarding.permission.authorized")} + + ); + } + + return ( + + ); + }; + + const permissions = [ + { + key: "accessibility", + title: t("onboarding.permission.accessibility.title"), + description: t("onboarding.permission.accessibility.description"), + granted: accessibilityGranted, + onRequest: handleRequestAccessibility, + }, + { + key: "screenRecording", + title: t("onboarding.permission.screen_recording.title"), + description: t("onboarding.permission.screen_recording.description"), + granted: screenRecordingGranted, + onRequest: handleRequestScreenRecording, + }, + { + key: "fullDiskAccess", + title: t("onboarding.permission.full_disk_access.title"), + description: t("onboarding.permission.full_disk_access.description"), + granted: fullDiskAccessGranted, + onRequest: handleRequestFullDiskAccess, + }, + ]; + + return ( + + + {/* 区块 1:开源与隐私声明 */} + + WeCut +
+ + WeCut + + + {t("onboarding.privacy.description")} + +
+ + + +
+ + {/* 区块 2:macOS 权限引导 */} +
+ + {t("onboarding.permissions.title")} + + + {t("onboarding.permissions.description")} + + + {permissions.map(({ key, title, description, granted, onRequest }) => ( + + + + +
+ {title} +
+ {description} +
+
+ {renderPermissionStatus(granted, onRequest)} +
+
+ ))} +
+
+ + {/* 区块 3:Wegent Key 配置(可选) */} + + + +
+ {t("onboarding.wegent.title")} + + {t("onboarding.wegent.description")} + + +
+
+
+
+ + {/* 底部固定操作区 */} +
+ + {!allGranted && ( + + {t("onboarding.complete.hint")} + + )} + + + + +
+
+ ); +}; + +export default Onboarding; diff --git a/src/pages/Preference/index.tsx b/src/pages/Preference/index.tsx index c1a483df..dd65f27f 100644 --- a/src/pages/Preference/index.tsx +++ b/src/pages/Preference/index.tsx @@ -11,6 +11,7 @@ import UpdateApp from "@/components/UpdateApp"; import { LISTEN_KEY } from "@/constants"; import { useRegister } from "@/hooks/useRegister"; import { useSubscribe } from "@/hooks/useSubscribe"; +import { useTauriListen } from "@/hooks/useTauriListen"; import { useTray } from "@/hooks/useTray"; import { isAutostart } from "@/plugins/autostart"; import { showWindow, toggleWindowVisible } from "@/plugins/window"; @@ -36,6 +37,13 @@ const Preference = () => { const [activeKey, setActiveKey] = useState("clipboard"); const contentRef = useRef(null); + // 监听来自其它窗口的跳转指令(如 Onboarding 跳转到指定 tab) + useTauriListen(LISTEN_KEY.PREFERENCE_NAVIGATE, ({ payload }) => { + if (payload) { + setActiveKey(payload); + } + }); + const { createTray } = useTray(); useMount(async () => { diff --git a/src/router/index.ts b/src/router/index.ts index b96ccd7b..2950c89f 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,5 +1,6 @@ import { createHashRouter } from "react-router-dom"; import Main from "@/pages/Main"; +import Onboarding from "@/pages/Onboarding"; import PinViewer from "@/pages/PinViewer"; import Preference from "@/pages/Preference"; import Screenshot from "@/pages/Screenshot"; @@ -11,6 +12,10 @@ export const router = createHashRouter([ Component: Main, path: "/", }, + { + Component: Onboarding, + path: "/onboarding", + }, { Component: Preference, path: "/preference", diff --git a/src/stores/global.ts b/src/stores/global.ts index f218cb84..bf66d8c9 100644 --- a/src/stores/global.ts +++ b/src/stores/global.ts @@ -7,6 +7,7 @@ export const globalStore = proxy({ showMenubarIcon: true, showTaskbarIcon: false, silentStart: false, + hasCompletedOnboarding: false, }, appearance: { diff --git a/src/types/store.d.ts b/src/types/store.d.ts index e2de621b..0f0a795e 100644 --- a/src/types/store.d.ts +++ b/src/types/store.d.ts @@ -17,6 +17,7 @@ export interface GlobalStore { silentStart: boolean; showMenubarIcon: boolean; showTaskbarIcon: boolean; + hasCompletedOnboarding: boolean; }; // 外观设置