├── public ├── icon │ ├── 128.png │ ├── 16.png │ ├── 32.png │ └── 96.png ├── wxt.svg ├── lottie │ └── coming-soon.json └── _locales │ ├── zh_HK │ └── messages.json │ ├── zh_TW │ └── messages.json │ └── zh_CN │ └── messages.json ├── docs ├── assets │ ├── badge-cr.png │ ├── badge-eg.png │ └── badge-fx.png ├── index.html └── privacy-policy │ └── index.html ├── safari_extensions └── Bark Sender │ ├── Bark Sender │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── mac-icon-16@1x.png │ │ │ ├── mac-icon-16@2x.png │ │ │ ├── mac-icon-32@1x.png │ │ │ ├── mac-icon-32@2x.png │ │ │ ├── mac-icon-128@1x.png │ │ │ ├── mac-icon-128@2x.png │ │ │ ├── mac-icon-256@1x.png │ │ │ ├── mac-icon-256@2x.png │ │ │ ├── mac-icon-512@1x.png │ │ │ ├── mac-icon-512@2x.png │ │ │ └── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── LargeIcon.imageset │ │ │ └── Contents.json │ ├── Resources │ │ ├── Icon.png │ │ ├── Style.css │ │ └── Script.js │ ├── Info.plist │ ├── Bark_Sender.entitlements │ ├── Bark Sender.entitlements │ ├── AppDelegate.swift │ ├── ViewController.swift │ └── en.lproj │ │ └── Main.html │ ├── Bark Sender.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── fei.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── Bark Sender Extension │ ├── Bark_Sender_Extension.entitlements │ ├── Info.plist │ └── SafariWebExtensionHandler.swift ├── entrypoints ├── popup │ ├── contexts │ │ ├── index.ts │ │ └── AppContext.tsx │ ├── main.tsx │ ├── index.html │ ├── utils │ │ ├── clipboardWrite.ts │ │ ├── notification.ts │ │ ├── languages.ts │ │ ├── extension.ts │ │ ├── history-service.ts │ │ ├── clipboard.ts │ │ ├── platform.ts │ │ ├── favicon-manager.ts │ │ ├── settings.ts │ │ ├── backup-crypto.ts │ │ └── storage.ts │ ├── components │ │ ├── DialogTransitions.tsx │ │ ├── LanguageSelector.tsx │ │ ├── DeviceAutocomplete.tsx │ │ ├── ThemeSelector.tsx │ │ ├── LanguageSelect.tsx │ │ ├── HistoryTableSkeleton.tsx │ │ ├── ShortcutTips.tsx │ │ ├── CacheSetting.tsx │ │ ├── OtherSettingsCard.tsx │ │ ├── PingButton.tsx │ │ ├── DeviceSelect.tsx │ │ ├── LocalSyncCard.tsx │ │ ├── FeatureSettings.tsx │ │ ├── PreviewCard.tsx │ │ ├── Layout.tsx │ │ └── DeviceSelectV2.tsx │ ├── style.css │ ├── hooks │ │ ├── useTheme.ts │ │ └── useStorage.ts │ ├── i18n.ts │ ├── types │ │ └── index.ts │ ├── App.css │ └── App.tsx └── background │ └── i18n-helper.ts ├── tsconfig.json ├── .gitignore ├── package.json ├── README-BUILD.md ├── .github └── workflows │ └── main.yml ├── README.md └── wxt.config.ts /public/icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/public/icon/128.png -------------------------------------------------------------------------------- /public/icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/public/icon/16.png -------------------------------------------------------------------------------- /public/icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/public/icon/32.png -------------------------------------------------------------------------------- /public/icon/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/public/icon/96.png -------------------------------------------------------------------------------- /docs/assets/badge-cr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/docs/assets/badge-cr.png -------------------------------------------------------------------------------- /docs/assets/badge-eg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/docs/assets/badge-eg.png -------------------------------------------------------------------------------- /docs/assets/badge-fx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/docs/assets/badge-fx.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /entrypoints/popup/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export { AppProvider, useAppContext } from './AppContext'; 2 | export type { AppContextType, AppContextState } from '../types'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.wxt/tsconfig.json", 3 | "compilerOptions": { 4 | "allowImportingTsExtensions": true, 5 | "jsx": "react-jsx" 6 | } 7 | } -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Resources/Icon.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ij369/bark-sender/HEAD/safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /entrypoints/popup/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './style.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | SFSafariWebExtensionConverterVersion 8 | 14.2 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Bark_Sender.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender Extension/Bark_Sender_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/LargeIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Bark Sender.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /entrypoints/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bark Sender 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .output 12 | stats.html 13 | stats-*.json 14 | .wxt 15 | web-ext.config.ts 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # hidden files 29 | /assets/ 30 | .env 31 | 32 | xcuserdata/ 33 | *.xcuserstate -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender.xcodeproj/xcuserdata/fei.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Bark Sender.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.Safari.web-extension 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Bark Sender 4 | // 5 | // Created by FEI on 11/11/2025. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | // Override point for customization after application launch. 15 | } 16 | 17 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/clipboardWrite.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 写入内容到剪切板 3 | * @param data - 文字内容或图片的 Blob/URL/Base64 4 | * @param type - 'txt'(默认)表示写文本,'img' 表示写图片 5 | */ 6 | export async function writeToClipboard( 7 | data: string | Blob, 8 | type: "txt" | "img" = "txt" 9 | ): Promise { 10 | try { 11 | if (type === "txt") { 12 | // 写入文字 13 | await navigator.clipboard.writeText(String(data)); 14 | } 15 | } catch (err) { 16 | console.error("写入剪切板失败:", err); 17 | throw new Error("Clipboard write failed: " + err); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/wxt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /entrypoints/popup/components/DialogTransitions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Slide from '@mui/material/Slide'; 3 | import Grow from '@mui/material/Grow'; 4 | import { TransitionProps } from '@mui/material/transitions'; 5 | 6 | export type SlideDirection = 'up' | 'down' | 'left' | 'right'; 7 | 8 | export const GrowTransition = React.forwardRef(function GrowTransition( 9 | props: TransitionProps & { 10 | children: React.ReactElement; 11 | }, 12 | ref: React.Ref, 13 | ) { 14 | return ; 15 | }); 16 | 17 | export const SlideTransition = (direction: SlideDirection = 'up') => 18 | React.forwardRef(function SlideTransition( 19 | props: TransitionProps & { 20 | children: React.ReactElement; 21 | }, 22 | ref: React.Ref, 23 | ) { 24 | return ; 25 | }); 26 | 27 | export const SlideUpTransition = SlideTransition('up'); 28 | export const SlideDownTransition = SlideTransition('down'); 29 | export const SlideLeftTransition = SlideTransition('left'); 30 | export const SlideRightTransition = SlideTransition('right'); 31 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/notification.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 取代 browser.notifications.create 用于显示系统级通知 3 | * @param title 通知标题 4 | * @param message 通知消息 5 | * @param isEssential 是否是必要通知(错误通知),默认false 6 | */ 7 | export async function showSYSNotification( 8 | title: string, 9 | message: string, 10 | isEssential: boolean = false 11 | ): Promise { 12 | try { 13 | const settingsResult = await browser.storage.local.get('bark_app_settings'); 14 | const settings = settingsResult.bark_app_settings || {}; 15 | 16 | // 检查系统通知设置 17 | if (settings.enableSystemNotifications === false) { 18 | return; 19 | } 20 | 21 | // 检查保留必要通知设置 22 | if (settings.keepEssentialNotifications === true && !isEssential) { 23 | return; 24 | } 25 | 26 | // 检查浏览器是否支持 notifications API(Safari 不支持) 27 | if (browser.notifications && browser.notifications.create) { 28 | await browser.notifications.create({ 29 | type: 'basic', 30 | iconUrl: '/icon/128.png', 31 | title, 32 | message 33 | }); 34 | } else { 35 | // Safari 不支持 36 | console.log(`通知: ${title} - ${message}`); 37 | } 38 | } catch (error) { 39 | console.error('创建通知失败:', error); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /entrypoints/popup/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | /* min-width: 320px; 32 | min-height: 100vh; */ 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | 52 | button:hover { 53 | border-color: #646cff; 54 | } 55 | 56 | @media (prefers-color-scheme: light) { 57 | :root { 58 | color: #213547; 59 | background-color: #ffffff; 60 | } 61 | 62 | a:hover { 63 | color: #747bff; 64 | } 65 | 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } -------------------------------------------------------------------------------- /entrypoints/popup/utils/languages.ts: -------------------------------------------------------------------------------- 1 | // 语言配置 2 | export interface LanguageOption { 3 | code: string; 4 | label: string; 5 | } 6 | 7 | // 支持的语言列表 8 | export const SUPPORTED_LANGUAGES: LanguageOption[] = [ 9 | { code: 'zh-CN', label: '简体中文' }, 10 | { code: 'zh-HK', label: '繁體中文(HK)' }, 11 | { code: 'zh-TW', label: '繁體中文(TW)' }, 12 | { code: 'en', label: 'English' } 13 | ]; 14 | 15 | // 获取支持的语言列表 16 | export function getSupportedLanguages(): LanguageOption[] { 17 | return SUPPORTED_LANGUAGES; 18 | } 19 | 20 | // 根据语言代码获取语言信息 21 | export function getLanguageByCode(code: string): LanguageOption | undefined { 22 | return SUPPORTED_LANGUAGES.find(lang => lang.code === code); 23 | } 24 | 25 | // 检查是否是支持的语言 26 | export function isSupportedLanguage(code: string): boolean { 27 | return SUPPORTED_LANGUAGES.some(lang => lang.code === code); 28 | } 29 | 30 | // 获取语言代码列表 31 | export function getSupportedLanguageCodes(): string[] { 32 | return SUPPORTED_LANGUAGES.map(lang => lang.code); 33 | } 34 | 35 | // 检测浏览器语言并返回支持的语言代码 36 | export function detectBrowserLanguage(): string { 37 | const browserLang = navigator.language || navigator.languages?.[0] || 'en'; 38 | 39 | // 如果不是中文,直接返回英文 40 | if (!browserLang.startsWith('zh')) { 41 | return 'en'; 42 | } 43 | 44 | // 检查是否是支持的中文变体 45 | if (isSupportedLanguage(browserLang)) { 46 | return browserLang; 47 | } 48 | 49 | // 回落到简中 50 | return 'zh-CN'; 51 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bark Sender", 3 | "description": "__MSG_ext_desc__", 4 | "private": true, 5 | "version": "1.7.0", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "wxt", 9 | "dev:firefox": "wxt -b firefox", 10 | "dev:edge": "wxt -b edge", 11 | "build": "wxt build", 12 | "build:chrome": "wxt build -b chrome", 13 | "build:firefox": "wxt build -b firefox", 14 | "build:edge": "wxt build -b edge", 15 | "zip": "wxt zip", 16 | "zip:chrome": "wxt zip -b chrome", 17 | "zip:firefox": "wxt zip -b firefox", 18 | "zip:edge": "wxt zip -b edge", 19 | "compile": "tsc --noEmit", 20 | "postinstall": "wxt prepare" 21 | }, 22 | "dependencies": { 23 | "@emotion/react": "^11.14.0", 24 | "@emotion/styled": "^11.14.1", 25 | "@mui/icons-material": "^7.2.0", 26 | "@mui/material": "^7.2.0", 27 | "@types/uuid": "^10.0.0", 28 | "ag-grid-community": "34.2.0", 29 | "ag-grid-react": "34.2.0", 30 | "dayjs": "^1.11.13", 31 | "gsap": "^3.13.0", 32 | "i18next": "^25.3.2", 33 | "i18next-browser-languagedetector": "^8.2.0", 34 | "idb": "^8.0.3", 35 | "lottie-web": "^5.13.0", 36 | "notistack": "^3.0.2", 37 | "react": "^19.1.0", 38 | "react-contexify": "^6.0.0", 39 | "react-dom": "^19.1.0", 40 | "react-i18next": "^15.6.0", 41 | "uuid": "^11.1.0" 42 | }, 43 | "devDependencies": { 44 | "@types/react": "^19.1.2", 45 | "@types/react-dom": "^19.1.3", 46 | "@wxt-dev/module-react": "^1.1.3", 47 | "typescript": "^5.8.3", 48 | "wxt": "^0.20.6" 49 | } 50 | } -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac-icon-16@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "mac-icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "mac-icon-32@1x.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "mac-icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "mac-icon-128@1x.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "mac-icon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "mac-icon-256@1x.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "mac-icon-256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "mac-icon-512@1x.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "mac-icon-512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/extension.ts: -------------------------------------------------------------------------------- 1 | import { detectBrowser } from './platform'; 2 | 3 | // 打开GitHub页面 4 | export function openGitHub() { 5 | const url = 'https://github.com/ij369/bark-sender'; 6 | window.open(url, '_blank'); 7 | } 8 | 9 | // 打开商店页面 10 | export function openStoreRating() { 11 | const browserType = detectBrowser(); 12 | let url = ''; 13 | 14 | switch (browserType) { 15 | case 'chrome': 16 | url = `https://chrome.google.com/webstore/detail/${browser.runtime.id}`; 17 | break; 18 | case 'firefox': 19 | url = `https://addons.mozilla.org/firefox/addon/bark-sender/`; 20 | break; 21 | case 'edge': 22 | url = `https://microsoftedge.microsoft.com/addons/detail/bark-sender/${browser.runtime.id}`; 23 | break; 24 | default: 25 | url = `https://github.com/ij369/bark-sender`; 26 | break; 27 | } 28 | window.open(url, '_blank'); 29 | } 30 | 31 | // 打开反馈页面 32 | export function openFeedback() { 33 | const url = 'https://github.com/ij369/bark-sender/issues'; 34 | window.open(url, '_blank'); 35 | } 36 | 37 | export function openTelegramChannel() { 38 | const url = 'https://t.me/s/bark_sender'; 39 | window.open(url, '_blank'); 40 | } 41 | 42 | export function openOfficialWebsite() { 43 | const url = 'https://bark-sender.uuphy.com'; 44 | window.open(url, '_blank'); 45 | } 46 | 47 | export function openBarkApp() { 48 | const url = 'https://apps.apple.com/app/bark-custom-notifications/id1403753865'; 49 | window.open(url, '_blank'); 50 | } 51 | 52 | export function openBarkWebsite() { 53 | const url = 'https://bark.day.app'; 54 | window.open(url, '_blank'); 55 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, Select, MenuItem, SelectChangeEvent } from '@mui/material'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { getSupportedLanguages } from '../utils/languages'; 4 | 5 | export default function LanguageSelector() { 6 | const { i18n } = useTranslation(); 7 | // 使用统一的语言列表 8 | const supportedLanguages = getSupportedLanguages(); 9 | 10 | const handleLanguageChange = async (event: SelectChangeEvent) => { 11 | const newLanguage = event.target.value; 12 | 13 | try { 14 | // 切换语言 15 | await i18n.changeLanguage(newLanguage); 16 | // 保存语言设置到storage 17 | await browser.storage.local.set({ language: newLanguage }); 18 | } catch (error) { 19 | console.error('切换语言失败:', error); 20 | } 21 | }; 22 | 23 | return ( 24 | 25 | 45 | 46 | ); 47 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/DeviceAutocomplete.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Autocomplete, TextField, Box, Typography } from '@mui/material'; 3 | import { Device } from '../types'; 4 | 5 | interface DeviceAutocompleteProps { 6 | devices: Device[]; 7 | selectedDevice: Device | null; 8 | onDeviceChange: (device: Device | null) => void; 9 | label?: string; 10 | placeholder?: string; 11 | } 12 | 13 | export default function DeviceAutocomplete({ 14 | devices, 15 | selectedDevice, 16 | onDeviceChange, 17 | label = '选择设备', 18 | placeholder = '请选择一个设备' 19 | }: DeviceAutocompleteProps) { 20 | return ( 21 | onDeviceChange(newValue)} 26 | getOptionLabel={(option) => option.alias} 27 | renderOption={(props, option) => ( 28 | 29 |
30 | {option.alias} 31 | 32 | {option.apiURL} 33 | 34 |
35 |
36 | )} 37 | renderInput={(params) => ( 38 | 45 | )} 46 | isOptionEqualToValue={(option, value) => option.id === value.id} 47 | noOptionsText="暂无设备,请先在配置页面添加设备" 48 | /> 49 | ); 50 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToggleButtonGroup, ToggleButton } from '@mui/material'; 3 | import LightModeIcon from '@mui/icons-material/LightMode'; 4 | import DarkModeIcon from '@mui/icons-material/DarkMode'; 5 | import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness'; 6 | import { useTranslation } from 'react-i18next'; 7 | import { ThemeMode } from '../types'; 8 | 9 | interface ThemeSelectorProps { 10 | themeMode: ThemeMode; 11 | onThemeChange: (mode: ThemeMode) => void; 12 | } 13 | 14 | export default function ThemeSelector({ themeMode, onThemeChange }: ThemeSelectorProps) { 15 | const { t } = useTranslation(); 16 | 17 | return ( 18 | { 22 | if (newMode !== null) { 23 | onThemeChange(newMode); 24 | } 25 | }} 26 | aria-label="theme mode" 27 | size="small" 28 | > 29 | 30 | 31 | {/* 浅色 */} 32 | {t('settings.theme.light')} 33 | 34 | 35 | 36 | {/* 深色 */} 37 | {t('settings.theme.dark')} 38 | 39 | 40 | 41 | {/* 跟随系统 */} 42 | {t('settings.theme.system')} 43 | 44 | 45 | ); 46 | } -------------------------------------------------------------------------------- /entrypoints/popup/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { ThemeMode } from '../types'; 3 | import { getAppSettings, updateAppSetting } from '../utils/settings'; 4 | 5 | export function useTheme() { 6 | const [themeMode, setThemeMode] = useState('system'); 7 | const [systemIsDark, setSystemIsDark] = useState(false); 8 | const [loading, setLoading] = useState(true); 9 | 10 | // 检测系统主题 11 | useEffect(() => { 12 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 13 | setSystemIsDark(mediaQuery.matches); 14 | 15 | const handleChange = (e: MediaQueryListEvent) => { 16 | setSystemIsDark(e.matches); 17 | }; 18 | 19 | mediaQuery.addEventListener('change', handleChange); 20 | return () => mediaQuery.removeEventListener('change', handleChange); 21 | }, []); 22 | 23 | // 加载保存的主题设置 24 | useEffect(() => { 25 | const loadTheme = async () => { 26 | try { 27 | const settings = await getAppSettings(); 28 | setThemeMode(settings.themeMode || 'system'); 29 | } catch (error) { 30 | console.error('加载主题设置失败:', error); 31 | setThemeMode('system'); 32 | } finally { 33 | setLoading(false); 34 | } 35 | }; 36 | loadTheme(); 37 | }, []); 38 | 39 | // 更新主题模式 40 | const updateThemeMode = async (newMode: ThemeMode) => { 41 | try { 42 | await updateAppSetting('themeMode', newMode); 43 | setThemeMode(newMode); 44 | } catch (error) { 45 | console.error('更新主题设置失败:', error); 46 | } 47 | }; 48 | 49 | // 计算最终的主题模式 50 | const effectiveTheme = themeMode === 'system' 51 | ? (systemIsDark ? 'dark' : 'light') 52 | : themeMode; 53 | 54 | return { 55 | themeMode, 56 | effectiveTheme, 57 | systemIsDark, 58 | loading, 59 | updateThemeMode 60 | }; 61 | } -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Bark Sender 4 | // 5 | // Created by FEI on 11/11/2025. 6 | // 7 | 8 | import Cocoa 9 | import SafariServices 10 | import WebKit 11 | 12 | let extensionBundleIdentifier = "com.uuphy.bark-sender.Extension" 13 | 14 | class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler { 15 | 16 | @IBOutlet var webView: WKWebView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | self.webView.navigationDelegate = self 22 | 23 | self.webView.configuration.userContentController.add(self, name: "controller") 24 | 25 | self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) 26 | } 27 | 28 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 29 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in 30 | guard let state = state, error == nil else { 31 | // Insert code to inform the user that something went wrong. 32 | return 33 | } 34 | 35 | DispatchQueue.main.async { 36 | if #available(macOS 13, *) { 37 | webView.evaluateJavaScript("show(\(state.isEnabled), true)") 38 | } else { 39 | webView.evaluateJavaScript("show(\(state.isEnabled), false)") 40 | } 41 | } 42 | } 43 | } 44 | 45 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 46 | if (message.body as! String != "open-preferences") { 47 | return; 48 | } 49 | 50 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in 51 | if let error = error { 52 | print("Failed to open Safari extension preferences: \(error)") 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/history-service.ts: -------------------------------------------------------------------------------- 1 | import { recordPushHistory } from './database'; 2 | 3 | // 历史记录服务,监听来自 background script 的记录请求 4 | export function initHistoryService() { 5 | // 监听来自 background script 的消息 6 | browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: any) => { 7 | if (message.action === 'recordHistory') { 8 | handleRecordHistory(message) 9 | .then((result) => { 10 | sendResponse({ success: true, data: result }); 11 | }) 12 | .catch((error) => { 13 | sendResponse({ success: false, error: error.message }); 14 | }); 15 | return true; // 保持消息通道开放 16 | } 17 | }); 18 | } 19 | 20 | // 处理记录历史请求 21 | async function handleRecordHistory(message: any) { 22 | const { 23 | body, 24 | apiUrl, 25 | deviceName, 26 | response, 27 | method = 'GET', 28 | options = {} 29 | } = message; 30 | 31 | return await recordPushHistory( 32 | body, 33 | apiUrl, 34 | deviceName, 35 | response, 36 | method, 37 | options 38 | ); 39 | } 40 | 41 | // 从 background script 记录历史 42 | export async function recordHistoryFromBackground( 43 | body: string, 44 | apiUrl: string, 45 | deviceName: string, 46 | response: any, 47 | method: 'GET' | 'POST' = 'GET', 48 | options: { 49 | title?: string; 50 | sound?: string; 51 | url?: string; 52 | isEncrypted?: boolean; 53 | parameters?: any[]; 54 | } = {} 55 | ) { 56 | try { 57 | const result = await browser.runtime.sendMessage({ 58 | action: 'recordHistory', 59 | body, 60 | apiUrl, 61 | deviceName, 62 | response, 63 | method, 64 | options 65 | }); 66 | 67 | if (!result.success) { 68 | throw new Error(result.error); 69 | } 70 | 71 | return result.data; 72 | } catch (error) { 73 | console.error('记录历史失败:', error); 74 | // 不抛出错误,避免影响主要功能 75 | } 76 | } -------------------------------------------------------------------------------- /entrypoints/popup/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { detectBrowser } from './platform'; 2 | 3 | // 读取剪切板内容 4 | export async function readClipboard(): Promise { 5 | const browser = detectBrowser(); 6 | 7 | // Safari 使用 native messaging 读剪切板 8 | if (browser === 'safari') { 9 | try { 10 | return await readClipboardViaNativeMessaging(); 11 | } catch (error) { 12 | console.error('Safari native messaging 读取剪切板失败:', error); 13 | // 回退到标准 API 14 | } 15 | } 16 | 17 | try { 18 | // 使用标准 Clipboard API 19 | if (navigator.clipboard && navigator.clipboard.readText) { 20 | const text = await navigator.clipboard.readText(); 21 | return text || ''; 22 | } 23 | 24 | throw new Error('浏览器不支持剪切板API'); 25 | } catch (error) { 26 | console.error('读取剪切板失败:', error); 27 | 28 | // 如果是权限问题,提供更友好的错误信息 29 | if (error instanceof Error && error.name === 'NotAllowedError') { 30 | throw new Error('剪切板权限被拒绝,请允许扩展访问剪切板'); 31 | } 32 | 33 | throw new Error('无法读取剪切板内容,请检查权限设置'); 34 | } 35 | } 36 | 37 | // Safari 通过 native messaging 读取剪切板 38 | async function readClipboardViaNativeMessaging(): Promise { 39 | return new Promise((resolve, reject) => { 40 | try { 41 | // 在 Safari 中,通过 background script 与 native app 通信 42 | browser.runtime.sendMessage({ 43 | action: 'readClipboardFromNative' 44 | }, (response) => { 45 | if (browser.runtime.lastError) { 46 | console.error('与 background script 通信失败:', browser.runtime.lastError); 47 | reject(new Error('与 background script 通信失败')); 48 | return; 49 | } 50 | 51 | if (response && response.success && response.type === 'text') { 52 | resolve(response.data || ''); 53 | } else { 54 | const errorMsg = response?.data || '剪切板为空或包含不支持的内容'; 55 | reject(new Error(errorMsg)); 56 | } 57 | }); 58 | } catch (error) { 59 | console.error('发送消息到 background script 失败:', error); 60 | reject(new Error('无法与 background script 通信')); 61 | } 62 | }); 63 | } -------------------------------------------------------------------------------- /entrypoints/popup/utils/platform.ts: -------------------------------------------------------------------------------- 1 | import { PlatformType } from '../types'; 2 | 3 | /** 4 | * 检测当前用户的操作系统平台 5 | * @returns PlatformType 6 | */ 7 | export function detectPlatform(): PlatformType { 8 | if (typeof navigator === 'undefined') { 9 | return 'unknown'; 10 | } 11 | 12 | const userAgent = navigator.userAgent.toLowerCase(); 13 | const platform = navigator.platform?.toLowerCase() || ''; 14 | 15 | // 检测Mac设备 16 | if (platform.includes('mac') || userAgent.includes('mac os') || userAgent.includes('macintosh')) { 17 | return 'mac'; 18 | } 19 | 20 | // 检测Windows设备 21 | if (platform.includes('win') || userAgent.includes('windows')) { 22 | return 'windows'; 23 | } 24 | 25 | // 检测Linux设备 26 | if (platform.includes('linux') || userAgent.includes('linux')) { 27 | return 'linux'; 28 | } 29 | 30 | return 'unknown'; 31 | } 32 | 33 | /** 34 | * 判断是否为Apple设备 35 | * @param platform 平台类型 36 | * @returns boolean 37 | */ 38 | export function isAppleDevice(platform: PlatformType): boolean { 39 | return platform === 'mac'; 40 | } 41 | 42 | /** 43 | * 根据平台获取快捷键组合文本 44 | * @param platform 平台类型 45 | * @returns 快捷键配置对象 46 | */ 47 | export function getShortcutKeys(platform: PlatformType) { 48 | const isApple = isAppleDevice(platform); 49 | 50 | return { 51 | send: isApple ? '⌘ + ↩' : 'Ctrl + Enter', 52 | openExtension: isApple ? '⌘ + ⇧ + 8' : 'Ctrl + Shift + 8' 53 | }; 54 | } 55 | 56 | /** 57 | * 检测浏览器类型 58 | * @returns 'firefox' | 'chrome' | 'edge' | 'safari' | 'unknown' 59 | */ 60 | export function detectBrowser(): 'firefox' | 'chrome' | 'edge' | 'safari' | 'unknown' { 61 | if (typeof navigator === 'undefined') { 62 | return 'unknown'; 63 | } 64 | 65 | const userAgent = navigator.userAgent.toLowerCase(); 66 | 67 | // Firefox 68 | if (userAgent.includes('firefox')) { 69 | return 'firefox'; 70 | } 71 | 72 | // Edge 73 | if (userAgent.includes('edg/') || userAgent.includes('edge/')) { 74 | return 'edge'; 75 | } 76 | 77 | // Chrome 78 | if (userAgent.includes('chrome') && !userAgent.includes('edg')) { 79 | return 'chrome'; 80 | } 81 | 82 | // Safari 83 | if (userAgent.includes('safari') && !userAgent.includes('chrome')) { 84 | return 'safari'; 85 | } 86 | 87 | return 'unknown'; 88 | } -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/en.lproj/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bark Sender 9 | 10 | 11 | 12 | 13 | 14 |
15 | 28 | 29 |
30 |

Bark Sender

31 |

Push web content to your iPhone

32 |
33 |
34 | 35 |
36 |

37 | You can turn on Bark Sender's extension in Safari extension preferences. 38 |

39 |

40 | Bark Sender's extension is currently on. Thank you for using! 41 |

42 |

43 | Bark Sender's extension is currently off. You can turn it on in Safari extension preferences. 44 |

45 |
46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/privacy-policy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bark Sender 隐私权政策 9 | 40 | 41 | 42 | 43 |

Bark Sender 隐私权政策

44 | 45 |

本隐私权政策说明了 Bark Sender 浏览器扩展如何收集、使用和保护您的信息。我们高度重视您的隐私,并致力于保护您的个人数据。

46 | 47 |

1. 数据收集和使用

48 |
    49 |
  • 所有数据仅通过用户手动配置 Bark 服务器的 API URL 发送并且不会与除该 URL 所属域名以外的任何服务器通信
  • 50 |
  • 所有数据均存储在用户本地浏览器中
  • 51 |
  • 不会收集任何其他个人信息
  • 52 |
53 | 54 |

2. 数据存储

55 |
    56 |
  • 所有配置数据使用浏览器本地存储
  • 57 |
  • 不会上传到任何第三方服务器
  • 58 |
  • 用户可随时删除所有存储的数据
  • 59 |
60 | 61 |

3. 数据传输

62 |
    63 |
  • 仅在用户触发推送时,将消息内容发送到用户配置的 Bark 服务器
  • 64 |
  • 不会与其他任何服务器通信
  • 65 |
66 | 67 |

4. 用户权利

68 |
    69 |
  • 用户可随时查看、修改或删除已保存的设备配置
  • 70 |
  • 卸载扩展会自动清除所有存储的数据
  • 71 |
72 | 73 |

5. 权限使用说明

74 |
    75 |
  • storage: 用于存储用户的设备配置和应用设置
  • 76 |
  • contextMenus: 实现右键菜单快速发送功能
  • 77 |
  • activeTab: 获取当前标签页中选中的文本
  • 78 |
  • clipboardRead: 实现发送剪贴板内容功能
  • 79 |
  • notifications: 显示推送结果通知
  • 80 |
81 | 82 |

6. 联系方式

83 |

如果您对本隐私权政策有任何疑问或建议,请通过以下方式联系我们:

84 | 85 | feedback@uuphy.com 86 | 87 |

最后更新时间:2025年07月27日

88 | 89 | 90 | -------------------------------------------------------------------------------- /entrypoints/popup/components/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { IconButton, Menu, MenuItem, ListItemText, Tooltip } from '@mui/material'; 3 | import TranslateIcon from '@mui/icons-material/Translate'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { getSupportedLanguages } from '../utils/languages'; 6 | 7 | export default function LanguageSelect() { 8 | const { i18n } = useTranslation(); 9 | const [anchorEl, setAnchorEl] = useState(null); 10 | const open = Boolean(anchorEl); 11 | 12 | // 获取支持的语言列表 13 | const supportedLanguages = getSupportedLanguages(); 14 | 15 | const handleClick = (event: React.MouseEvent) => { 16 | setAnchorEl(event.currentTarget); 17 | }; 18 | 19 | const handleClose = () => { 20 | setAnchorEl(null); 21 | }; 22 | 23 | const handleLanguageSelect = async (languageCode: string) => { 24 | try { 25 | // 切换语言 26 | await i18n.changeLanguage(languageCode); 27 | // 保存语言设置到 storage 28 | await browser.storage.local.set({ language: languageCode }); 29 | handleClose(); 30 | } catch (error) { 31 | console.error('切换语言失败:', error); 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 | 38 | 47 | 48 | 49 | 50 | 59 | {supportedLanguages.map((language) => ( 60 | handleLanguageSelect(language.code)} 64 | > 65 | {language.label} 66 | 67 | ))} 68 | 69 | 70 | ); 71 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/HistoryTableSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Paper, 4 | TextField, 5 | useTheme, 6 | Stack 7 | } from '@mui/material'; 8 | import SearchIcon from '@mui/icons-material/Search'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | interface HistoryTableSkeletonProps { 12 | onSearch?: (searchText: string) => void; 13 | } 14 | 15 | export default function HistoryTableSkeleton({ }: HistoryTableSkeletonProps) { 16 | const { t } = useTranslation(); 17 | const theme = useTheme(); 18 | 19 | return ( 20 | <> 21 | {/* 搜索框 */} 22 | 23 | , 32 | } 33 | }} 34 | /> 35 | 36 | 37 | {/* 表格骨架屏 */} 38 | 48 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 73 | Loading... 74 | 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /entrypoints/popup/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | import { detectBrowserLanguage, isSupportedLanguage } from './utils/languages'; 5 | 6 | import translationEN from './locales/en/translation.json'; 7 | import translationZH from './locales/zh-CN/translation.json'; 8 | import translationZHHK from './locales/zh-HK/translation.json'; 9 | import translationZHTW from './locales/zh-TW/translation.json'; 10 | 11 | const resources = { 12 | en: { 13 | translation: translationEN 14 | }, 15 | 'zh-CN': { 16 | translation: translationZH 17 | }, 18 | 'zh-HK': { 19 | translation: translationZHHK 20 | }, 21 | 'zh-TW': { 22 | translation: translationZHTW 23 | } 24 | }; 25 | 26 | const initializeLanguage = async (): Promise => { 27 | try { 28 | const result = await browser.storage.local.get('language'); 29 | 30 | if (result.language && isSupportedLanguage(result.language)) { 31 | // 如果已有存储的语言设置且在支持范围内,使用存储的语言 32 | return result.language; 33 | } else { 34 | // 没有存储语言或存储的语言不支持,检测浏览器语言 35 | const detectedLang = detectBrowserLanguage(); 36 | 37 | // 存储检测到的语言 38 | await browser.storage.local.set({ language: detectedLang }); 39 | 40 | return detectedLang; 41 | } 42 | } catch (error) { 43 | console.error('初始化语言设置失败:', error); 44 | // 出错时默认使用英文并存储 45 | try { 46 | await browser.storage.local.set({ language: 'en' }); 47 | } catch (storageError) { 48 | console.error('存储默认语言失败:', storageError); 49 | } 50 | return 'en'; 51 | } 52 | }; 53 | 54 | // 同步初始化 i18n,避免异步导致的渲染问题 55 | i18n 56 | .use(LanguageDetector) 57 | .use(initReactI18next) 58 | .init({ 59 | resources, 60 | lng: 'en', // 临时默认语言,会被异步更新 61 | fallbackLng: { 62 | 'zh-HK': ['zh-TW', 'zh-CN', 'en'], 63 | 'zh-TW': ['zh-HK', 'zh-CN', 'en'], 64 | 'zh-CN': ['en'], 65 | default: ['en'] 66 | }, 67 | interpolation: { 68 | escapeValue: false 69 | }, 70 | detection: { 71 | order: ['navigator'], 72 | caches: [] 73 | } 74 | }); 75 | 76 | // 异步加载保存的语言设置 77 | const loadStoredLanguage = async () => { 78 | const selectedLanguage = await initializeLanguage(); // 初始化语言设置 79 | 80 | if (selectedLanguage !== i18n.language) { 81 | try { 82 | await i18n.changeLanguage(selectedLanguage); 83 | } catch (error) { 84 | console.error('切换语言失败:', error); 85 | } 86 | } 87 | }; 88 | 89 | // 在i18n初始化完成后加载保存的语言设置 90 | loadStoredLanguage(); 91 | 92 | export default i18n; -------------------------------------------------------------------------------- /entrypoints/popup/components/ShortcutTips.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Typography } from '@mui/material'; 3 | 4 | interface ShortcutTip { 5 | key?: string; 6 | description: string; 7 | } 8 | 9 | interface ShortcutTipsProps { 10 | tips: ShortcutTip[]; 11 | interval?: number; 12 | } 13 | 14 | export default function ShortcutTips({ 15 | tips, 16 | interval = 10000 // 默认10秒切换一次 17 | }: ShortcutTipsProps) { 18 | const [currentIndex, setCurrentIndex] = useState(0); 19 | const [isFlipping, setIsFlipping] = useState(false); 20 | const containerRef = useRef(null); 21 | 22 | useEffect(() => { 23 | if (tips.length <= 1) return; 24 | 25 | const flipToNext = () => { 26 | setIsFlipping(true); 27 | 28 | setTimeout(() => { 29 | setCurrentIndex((prev) => (prev + 1) % tips.length); 30 | }, 250); // 动画为总时长的一半 31 | 32 | setTimeout(() => { 33 | // 重置动画状态 34 | setIsFlipping(false); 35 | }, 500); // 动画总时长 36 | }; 37 | 38 | const intervalId = setInterval(flipToNext, interval); 39 | 40 | return () => { 41 | clearInterval(intervalId); 42 | }; 43 | }, [tips, interval]); 44 | 45 | if (!tips.length) return null; 46 | 47 | return ( 48 |
57 |
71 | 78 | {tips[currentIndex].key ? ( 79 | // 快捷键 + 描述 80 | <>Tips: {tips[currentIndex].key} {tips[currentIndex].description} 81 | ) : ( 82 | // 纯文本内容 83 | <>Tips: {tips[currentIndex].description} 84 | )} 85 | 86 |
87 |
88 | ); 89 | } -------------------------------------------------------------------------------- /entrypoints/background/i18n-helper.ts: -------------------------------------------------------------------------------- 1 | // background 专用 i18n 工具函数 2 | // 与 React i18next 并存,读取相同的语言设置 3 | 4 | // 支持的语言代码映射(React i18next -> Chrome i18n) 5 | const LANGUAGE_MAP: Record = { 6 | 'zh-CN': 'zh_CN', 7 | 'zh-TW': 'zh_TW', 8 | 'zh-HK': 'zh_HK', 9 | 'en': 'en' 10 | }; 11 | 12 | // 默认语言(与manifest的default_locale保持一致) 13 | const DEFAULT_LANGUAGE = 'en'; 14 | 15 | // 缓存当前语言 16 | let currentLanguage: string = DEFAULT_LANGUAGE; 17 | 18 | // 初始化语言设置 19 | export async function initBackgroundI18n(): Promise { 20 | try { 21 | // 读取用户保存的语言设置(与React i18next使用相同的存储键) 22 | const result = await browser.storage.local.get('language'); 23 | 24 | if (result.language && Object.keys(LANGUAGE_MAP).includes(result.language)) { 25 | currentLanguage = result.language; 26 | } else { 27 | // 如果没有保存的语言设置,检测浏览器语言 28 | const browserLang = detectBrowserLanguage(); 29 | currentLanguage = browserLang; 30 | 31 | // 保存检测到的语言设置 32 | await browser.storage.local.set({ language: browserLang }); 33 | } 34 | 35 | // Safari 下会乱码 36 | // console.debug('Background i18n initialized, current language:', currentLanguage); 37 | } catch (error) { 38 | // console.error('Failed to initialize background i18n:', error); 39 | currentLanguage = DEFAULT_LANGUAGE; 40 | } 41 | } 42 | 43 | // 检测浏览器语言(popup的逻辑) 44 | function detectBrowserLanguage(): string { 45 | const browserLang = navigator.language || 'en'; 46 | 47 | // 如果不是中文,直接返回英文 48 | if (!browserLang.startsWith('zh')) { 49 | return 'en'; 50 | } 51 | 52 | // 检查是否是支持的中文变体 53 | if (Object.keys(LANGUAGE_MAP).includes(browserLang)) { 54 | return browserLang; 55 | } 56 | 57 | // 回落到简中 58 | return 'zh-CN'; 59 | } 60 | 61 | // 监听语言设置变化 62 | export function watchLanguageChanges(): void { 63 | browser.storage.onChanged.addListener((changes) => { 64 | if (changes.language) { 65 | const newLanguage = changes.language.newValue; 66 | if (newLanguage && Object.keys(LANGUAGE_MAP).includes(newLanguage)) { 67 | currentLanguage = newLanguage; 68 | console.debug('Background i18n 语言已更新:', currentLanguage); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | // 获取本地化消息 75 | export function getMessage(key: string, substitutions?: string | string[]): string { 76 | try { 77 | // 使用Chrome原生i18n API 78 | const message = (browser.i18n as any).getMessage(key, substitutions); 79 | 80 | if (message) { 81 | return message; 82 | } 83 | 84 | // 如果没有找到消息,返回key作为fallback 85 | console.warn(`未找到i18n消息: ${key}`); 86 | return key; 87 | } catch (error) { 88 | console.error(`获取i18n消息失败: ${key}`, error); 89 | return key; 90 | } 91 | } 92 | 93 | // 获取当前语言 94 | export function getCurrentLanguage(): string { 95 | return currentLanguage; 96 | } 97 | 98 | // 检查是否为中文语言 99 | export function isChineseLanguage(): boolean { 100 | return currentLanguage.startsWith('zh'); 101 | } -------------------------------------------------------------------------------- /entrypoints/popup/types/index.ts: -------------------------------------------------------------------------------- 1 | // 设备信息接口 2 | export interface Device { 3 | id: string; // 时间戳作为唯一标识 4 | alias: string; // 设备别名 5 | apiURL: string; // API URL 6 | createdAt: string; // 创建时间 YYYY-MM-DD HH:mm:ss 7 | timestamp: number; // 时间戳 8 | authorization?: { 9 | type: 'basic'; 10 | user: string; 11 | pwd: string; 12 | value: string; // `basic btoa(${username}:${password})` 13 | }; 14 | server?: string; // 服务器地址 为 API v2 批量推送使用 15 | deviceKey?: string; // 设备密钥 为 API v2 批量推送使用 16 | } 17 | 18 | // 推送响应接口 19 | export interface PushResponse { 20 | code: number; 21 | message: string; 22 | timestamp: number; 23 | } 24 | 25 | // 页面标识 26 | export type TabValue = 'send' | 'history' | 'settings'; 27 | 28 | // 主题模式类型 29 | export type ThemeMode = 'light' | 'dark' | 'system'; 30 | 31 | // 加密算法类型 32 | export type EncryptionAlgorithm = 'AES256' | 'AES192' | 'AES128'; 33 | 34 | // 加密模式类型 35 | export type EncryptionMode = 'CBC' | 'GCM'; 36 | 37 | // 填充模式类型 38 | export type PaddingMode = 'pkcs7'; 39 | 40 | // 加密配置接口 41 | export interface EncryptionConfig { 42 | algorithm: EncryptionAlgorithm; 43 | mode: EncryptionMode; 44 | padding: PaddingMode; 45 | key: string; 46 | } 47 | 48 | // 铃声接口 49 | export interface Sound { 50 | name: string; 51 | duration: number; // 持续时间 (ms) 52 | old?: boolean; // 是否为旧铃声 53 | } 54 | 55 | // 应用设置接口 56 | export interface AppSettings { 57 | enableContextMenu: boolean; 58 | enableInspectSend: boolean; 59 | themeMode: ThemeMode; 60 | enableEncryption: boolean; 61 | encryptionConfig?: EncryptionConfig; 62 | sound?: string; 63 | enableBasicAuth: boolean; 64 | enableCustomAvatar?: boolean; // 是否启用自定义头像 65 | barkAvatarUrl?: string; // 自定义头像URL 66 | enableApiV2?: boolean; // 是否启用API v2 67 | enableAdvancedParams?: boolean; // 是否启用完整参数配置 68 | advancedParamsJson?: string; // 完整参数配置的 JSON 69 | enableSpeedMode?: boolean; // 是否启用极速模式 70 | speedModeCountdown?: number; // 极速模式倒计时时间(ms) 71 | enableFaviconIcon?: boolean; // 是否启用 favicon 作为 icon 72 | faviconApiUrl?: string; // favicon 接口 URL 模板 73 | enableSystemNotifications?: boolean; // 是否启用系统通知 74 | keepEssentialNotifications?: boolean; // 是否保留必要通知(仅显示错误通知) 75 | enableFileCache?: boolean; // 是否启用文件缓存 76 | } 77 | // 平台类型 78 | export type PlatformType = 'mac' | 'windows' | 'linux' | 'unknown'; 79 | 80 | // App上下文状态接口 81 | export interface AppContextState { 82 | platform: PlatformType; 83 | isAppleDevice: boolean; 84 | shortcutKeys: { 85 | send: string; // 发送快捷键组合 86 | openExtension: string; // 打开扩展快捷键组合 87 | }; 88 | // 加密相关 89 | appSettings: AppSettings | null; 90 | loading: boolean; 91 | } 92 | 93 | // App上下文接口 94 | export interface AppContextType extends AppContextState { 95 | // 加密相关 96 | toggleEncryption: () => Promise; 97 | updateEncryptionConfig: (config: EncryptionConfig) => Promise; 98 | updateAppSetting: (key: K, value: AppSettings[K]) => Promise; 99 | reloadSettings: () => Promise; 100 | shouldShowEncryptionToggle: boolean; 101 | cleanupBlobs: () => void; // blob 管理 102 | } 103 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/favicon-manager.ts: -------------------------------------------------------------------------------- 1 | import { fileCacheManager } from './database'; 2 | import { getAppSettings } from './settings'; 3 | 4 | class FaviconManager { 5 | private blobUrls: Set = new Set(); 6 | 7 | // 预请求并缓存 favicon 8 | async cacheFavicon(url: string): Promise { 9 | try { 10 | const settings = await getAppSettings(); 11 | if (!settings.enableFileCache) { 12 | return; // 未启用文件缓存 13 | } 14 | 15 | const response = await fetch(url); 16 | if (!response.ok) { 17 | throw new Error(`HTTP ${response.status}`); 18 | } 19 | 20 | const arrayBuffer = await response.arrayBuffer(); 21 | await fileCacheManager.saveFavicon(url, arrayBuffer); 22 | console.debug('Favicon 已缓存:', url); 23 | } catch (error) { 24 | console.error('缓存 favicon 失败:', url, error); 25 | } 26 | } 27 | 28 | // 获取 favicon,优先从缓存获取 29 | async getFavicon(url: string, timestamp?: number): Promise { 30 | try { 31 | const settings = await getAppSettings(); 32 | if (!settings.enableFileCache) { 33 | return url; // 未启用文件缓存,返回原始 URL 34 | } 35 | 36 | // 尝试从缓存获取 37 | const cachedUrl = await fileCacheManager.getFavicon(url, timestamp); 38 | if (cachedUrl) { 39 | // 记录 blob URL 以便后续清理 40 | this.blobUrls.add(cachedUrl); 41 | return cachedUrl; 42 | } 43 | 44 | // 缓存中没有,尝试预请求并缓存 45 | await this.cacheFavicon(url); 46 | 47 | // 再次尝试从缓存获取 48 | const newCachedUrl = await fileCacheManager.getFavicon(url, timestamp); 49 | if (newCachedUrl) { 50 | this.blobUrls.add(newCachedUrl); 51 | return newCachedUrl; 52 | } 53 | 54 | // 都失败了,返回原始 URL 55 | return url; 56 | } catch (error) { 57 | console.error('获取 favicon 失败:', url, error); 58 | return url; // 出错时返回原始 URL 59 | } 60 | } 61 | 62 | // 清理所有创建的 blob URLs 63 | cleanupBlobUrls(): void { 64 | this.blobUrls.forEach(url => { 65 | try { 66 | URL.revokeObjectURL(url); 67 | } catch (error) { 68 | console.warn('清理 blob URL 失败:', url, error); 69 | } 70 | }); 71 | this.blobUrls.clear(); 72 | console.debug('已清理所有 blob URLs'); 73 | } 74 | } 75 | 76 | export const faviconManager = new FaviconManager(); 77 | 78 | export async function getFaviconUrl(url: string, timestamp?: number): Promise { 79 | return await faviconManager.getFavicon(url, timestamp); 80 | } 81 | 82 | export function cleanupFaviconBlobs(): void { 83 | faviconManager.cleanupBlobUrls(); 84 | } 85 | 86 | export async function cacheFaviconUrl(url: string): Promise { 87 | await faviconManager.cacheFavicon(url); 88 | } 89 | 90 | export async function getImageByPushID(pushID: string): Promise { 91 | return await fileCacheManager.getImageByPushID(pushID); 92 | } 93 | 94 | export async function deleteImagesBefore(timestamp: number): Promise { 95 | await fileCacheManager.deleteImagesBefore(timestamp); 96 | } 97 | -------------------------------------------------------------------------------- /entrypoints/popup/components/CacheSetting.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Stack, 4 | FormControlLabel, 5 | Switch, 6 | Snackbar, 7 | CircularProgress, 8 | } from '@mui/material'; 9 | import { useTranslation } from 'react-i18next'; 10 | import { useAppContext } from '../contexts/AppContext'; 11 | import { fileCacheManager } from '../utils/database'; 12 | 13 | export default function CacheSetting() { 14 | const { t } = useTranslation(); 15 | const { appSettings, updateAppSetting } = useAppContext(); 16 | const [toast, setToast] = useState<{ open: boolean, message: string }>({ open: false, message: '' }); 17 | const [isToggling, setIsToggling] = useState(false); 18 | 19 | // 处理文件缓存开关切换 20 | const handleFileCacheToggle = async (enabled: boolean) => { 21 | if (isToggling) return; // 防止重复操作 22 | 23 | setIsToggling(true); 24 | try { 25 | if (enabled) { 26 | // 开启缓存:先创建数据库和表结构 27 | console.debug('正在创建文件缓存数据库...'); 28 | await fileCacheManager.init(); // 这会自动创建数据库和表 29 | console.debug('文件缓存数据库创建成功'); 30 | 31 | await updateAppSetting('enableFileCache', true); 32 | setToast({ 33 | open: true, 34 | message: t('settings.cache.cache_enabled') 35 | }); 36 | } else { 37 | // 关闭缓存:直接销毁整个 BarkSenderFileDB 数据库 38 | await fileCacheManager.destroy(); 39 | 40 | await updateAppSetting('enableFileCache', false); 41 | setToast({ 42 | open: true, 43 | message: t('settings.cache.cache_disabled') 44 | }); 45 | } 46 | } catch (error) { 47 | console.error('更新文件缓存设置失败:', error); 48 | setToast({ 49 | open: true, 50 | message: t('common.error_update', { message: error instanceof Error ? error.message : '未知错误' }) 51 | }); 52 | } finally { 53 | setIsToggling(false); 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 | 60 | 63 | handleFileCacheToggle(e.target.checked)} 66 | disabled={isToggling} 67 | color='warning' 68 | /> 69 | 70 | 71 | } 72 | label={t('settings.cache.enable')} 73 | sx={{ userSelect: 'none' }} 74 | /> 75 | {isToggling && } 76 | 77 | 78 | {/* Toast提示 */} 79 | setToast({ ...toast, open: false })} 83 | message={toast.message} 84 | /> 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/settings.ts: -------------------------------------------------------------------------------- 1 | declare const chrome: typeof browser; 2 | 3 | import { AppSettings } from '../types'; 4 | 5 | // 设置存储键名 6 | const SETTINGS_KEY = 'bark_app_settings'; 7 | 8 | // 默认自定义参数配置 9 | export const DEFAULT_ADVANCED_PARAMS = { 10 | title: "", 11 | subtitle: "", 12 | image: "", 13 | level: "", 14 | volume: "5", 15 | badge: "", 16 | call: "", 17 | autoCopy: "1", 18 | copy: "", 19 | group: "", 20 | isArchive: "", 21 | url: "", 22 | action: "" 23 | }; 24 | 25 | // 默认设置 26 | export const DEFAULT_SETTINGS: AppSettings = { 27 | enableContextMenu: true, 28 | enableInspectSend: true, 29 | themeMode: 'system', 30 | enableEncryption: false, 31 | encryptionConfig: { 32 | algorithm: 'AES256', 33 | mode: 'CBC', 34 | padding: 'pkcs7', 35 | key: '' 36 | }, 37 | sound: undefined, // 推送铃声,默认不设置 38 | enableBasicAuth: false, // Basic Auth,默认关闭 39 | enableApiV2: false, // API v2,默认关闭 40 | enableAdvancedParams: false, // 完整参数配置,默认关闭 41 | advancedParamsJson: JSON.stringify(DEFAULT_ADVANCED_PARAMS, null, 2), // 默认自定义参数JSON 42 | enableSpeedMode: false, // 极速模式,默认关闭 43 | speedModeCountdown: 3000, // 极速模式倒计时,默认3秒 44 | enableFaviconIcon: false, // 启用 favicon 作为 icon,默认关闭 45 | faviconApiUrl: '', // favicon 接口 URL 模板 46 | enableSystemNotifications: true, // 启用系统通知,默认true 47 | keepEssentialNotifications: true, // 保有必要通知,默认true 48 | enableFileCache: true, // 文件缓存,默认开启 49 | }; 50 | 51 | // 获取浏览器存储API 52 | async function getStorage() { 53 | if (typeof browser !== 'undefined' && browser.storage) { 54 | return browser.storage; 55 | } 56 | if (typeof chrome !== 'undefined' && chrome.storage) { 57 | return chrome.storage; 58 | } 59 | throw new Error('无法获取浏览器存储API'); 60 | } 61 | 62 | // 获取应用设置 63 | export async function getAppSettings(): Promise { 64 | try { 65 | const storage = await getStorage(); 66 | const result = await storage.local.get(SETTINGS_KEY); 67 | const settings = result[SETTINGS_KEY]; 68 | 69 | // 如果没有设置,直接返回默认设置 70 | if (!settings) { 71 | return DEFAULT_SETTINGS; 72 | } 73 | 74 | /* 使用默认设置填充缺失的字段 75 | // 后续新增设置项时,使用 DEFAULT_SETTINGS 兜底 76 | // 避免存储操作 */ 77 | return { ...DEFAULT_SETTINGS, ...settings }; 78 | } catch (error) { 79 | console.error('获取应用设置失败:', error); 80 | return DEFAULT_SETTINGS; 81 | } 82 | } 83 | 84 | // 保存应用设置 85 | export async function saveAppSettings(settings: AppSettings): Promise { 86 | try { 87 | const storage = await getStorage(); 88 | await storage.local.set({ [SETTINGS_KEY]: settings }); 89 | // 触发设置变更事件,通知background更新右键菜单 90 | await storage.local.set({ 'settings_updated': Date.now() }); 91 | } catch (error) { 92 | console.error('保存应用设置失败:', error); 93 | throw error; 94 | } 95 | } 96 | 97 | // 更新单个设置项 98 | export async function updateAppSetting( 99 | key: K, 100 | value: AppSettings[K] 101 | ): Promise { 102 | try { 103 | const currentSettings = await getAppSettings(); 104 | const newSettings = { ...currentSettings, [key]: value }; 105 | await saveAppSettings(newSettings); 106 | } catch (error) { 107 | console.error('更新设置失败:', error); 108 | throw error; 109 | } 110 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/OtherSettingsCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Box, 4 | Typography, 5 | Stack, 6 | Paper, 7 | IconButton, 8 | Dialog, 9 | AppBar, 10 | Toolbar, 11 | Button 12 | } from '@mui/material'; 13 | import SettingsIcon from '@mui/icons-material/Settings'; 14 | import NavigateNextIcon from '@mui/icons-material/NavigateNext'; 15 | import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; 16 | import { SlideLeftTransition } from './DialogTransitions'; 17 | import { useTranslation } from 'react-i18next'; 18 | import { ThemeMode } from '../types'; 19 | import OtherSettings from './OtherSettings'; 20 | 21 | 22 | interface OtherSettingsCardProps { 23 | themeMode: ThemeMode; 24 | onThemeChange: (mode: ThemeMode) => void; 25 | onError: (error: string) => void; 26 | onToast: (message: string) => void; 27 | } 28 | 29 | export default function OtherSettingsCard({ themeMode, onThemeChange, onError, onToast }: OtherSettingsCardProps) { 30 | const { t } = useTranslation(); 31 | const [dialogOpen, setDialogOpen] = useState(false); 32 | 33 | const handleClose = () => { 34 | setDialogOpen(false); 35 | }; 36 | 37 | return ( 38 | <> 39 | 40 | 52 | 53 | 54 | 63 | 64 | 65 | 71 | 72 | 73 | 74 | {/* 其他设置 */} 75 | {t('settings.title')} 76 | 77 | 78 | 79 | 80 | {/* 内容区域 */} 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /README-BUILD.md: -------------------------------------------------------------------------------- 1 | # Bark Sender - Build Instructions 2 | 3 | ## English Version 4 | 5 | ### Build Requirements 6 | 7 | This project uses **WXT** (Web Extension Toolkit) to build browser extensions for **Firefox**, **Chrome**, and **Edge**. The following programs and versions are required for building: 8 | 9 | #### Operating System Requirements 10 | - **macOS**: 12.7+ (Monterey or later) 11 | - **Linux**: Ubuntu 18.04+ or equivalent distributions 12 | 13 | #### Required Programs and Versions 14 | 15 | 1. **Node.js**: Version 20.0.0 or higher 16 | - Download from: https://nodejs.org/ 17 | - Verify installation: `node --version` 18 | 19 | 2. **pnpm**: Version 9.0.0 or higher (Package Manager) 20 | - Install via npm: `npm install -g pnpm` 21 | - Or install via standalone: https://pnpm.io/installation 22 | - Verify installation: `pnpm --version` 23 | 24 | 3. **TypeScript**: Version 5.8.3 (included in devDependencies) 25 | - Automatically installed via pnpm 26 | 27 | 4. **WXT**: Version 0.20.6 (Web Extension Toolkit) 28 | - Automatically installed via pnpm 29 | - Main build tool for the extension 30 | 31 | #### Build Environment Setup 32 | 33 | 1. **Clone/Extract the source code** 34 | 2. **Navigate to project directory** 35 | 3. **Set execute permissions for build script**: 36 | ```bash 37 | chmod +x build.sh 38 | ``` 39 | 4. **Install dependencies**: `pnpm install` 40 | 5. **Run build script**: Execute the provided `build.sh` script 41 | 42 | #### Build Process 43 | 44 | The build process consists of the following steps: 45 | 46 | 1. **Dependency Installation**: Install all required packages via pnpm 47 | 2. **TypeScript Compilation**: Compile TypeScript source files 48 | 3. **WXT Build**: Build extensions for both Firefox and Chrome/Edge targets 49 | 4. **Packaging**: Create distributable ZIP files for all browsers 50 | 51 | #### Build Script Execution 52 | 53 | **First, make the script executable:** 54 | ```bash 55 | chmod +x build.sh 56 | ``` 57 | 58 | **Then run the build script:** 59 | ```bash 60 | ./build.sh 61 | ``` 62 | 63 | This script will: 64 | - Automatically set execute permissions if needed 65 | - Install all dependencies 66 | - Build extensions for Firefox and Chrome/Edge 67 | - Create ZIP packages for all browsers 68 | - Output the following files in the `.output` folder: 69 | - `bark-sender--firefox.zip` (Firefox) 70 | - `bark-sender--chrome.zip` (Chrome/ Edge) 71 | 72 | #### Final Output 73 | 74 | After successful build, you will find the extension packages at: 75 | - **Firefox**: `.output/bark-sender--firefox.zip` 76 | - **Chrome/Edge**: `.output/bark-sender--chrome.zip` 77 | - **Location**: Project root `.output` directory 78 | 79 | ✅ System Compatibility: Passed testing on macOS 12.7.6 80 | 81 | ### Source Code Repository 82 | 83 | 👉 https://github.com/ij369/bark-sender 84 | 85 | This repository contains all source files, build scripts, and dependency declarations required to reproduce the extension package. 86 | 87 | 88 | --- 89 | 90 | ## Dependencies Information 91 | 92 | ### Main Dependencies 93 | - **React**: 19.1.0 - UI framework 94 | - **Material-UI**: 7.2.0 - UI components 95 | - **TypeScript**: 5.8.3 - Type system 96 | - **WXT**: 0.20.6 - Extension build toolkit 97 | - **i18next**: 25.3.2 - Internationalization 98 | 99 | ### Development Dependencies 100 | All development dependencies are automatically installed via `pnpm install` and are required for the build process. 101 | 102 | For complete dependency list, refer to `package.json` in the project root. -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Extension 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-release: 11 | name: Build and Release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Node.js 20 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | 23 | - name: Set up pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | version: 9 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Make build.sh executable 32 | run: chmod +x ./build.sh 33 | 34 | - name: Run build script 35 | run: ./build.sh 36 | 37 | - name: Collect Git Info 38 | id: gitinfo 39 | run: | 40 | echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 41 | echo "build_time=$(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT 42 | 43 | git fetch --tags --force 44 | 45 | PREVIOUS_TAG=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "") 46 | 47 | if [ -n "$PREVIOUS_TAG" ]; then 48 | # 每行显示短SHA、提交信息和作者 49 | COMMIT_LOG=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"- %h %s (%an)") 50 | echo "previous_tag=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT 51 | else 52 | COMMIT_LOG=$(git log -n 5 --pretty=format:"- %h %s (%an)") 53 | echo "previous_tag=Initial Release" >> $GITHUB_OUTPUT 54 | fi 55 | 56 | { 57 | echo "commits<> "$GITHUB_OUTPUT" 61 | 62 | - name: Create GitHub Release 63 | if: startsWith(github.ref, 'refs/tags/') 64 | uses: softprops/action-gh-release@v2 65 | with: 66 | draft: false 67 | files: | 68 | .output/*-chrome.zip 69 | .output/*-firefox.zip 70 | .output/*-edge.zip 71 | tag_name: ${{ github.ref_name }} 72 | name: Release ${{ github.ref_name }} 73 | body: | 74 | ## 🍻 Bark Sender New Release | 新版发布 75 | 76 | - **Firefox**: `bark-sender-*-firefox.zip` 77 | - **Chrome**: `bark-sender-*-chrome.zip` 78 | - **Edge**: `bark-sender-*-edge.zip` 79 | 80 | ### 📖 Installation Guide | 安装说明 81 | 1. Download the appropriate extension zip | 下载对应浏览器的扩展包 82 | 2. Extract to a local folder | 解压缩到本地文件夹 83 | 3. Go to your browser's extension page, enable "Load unpacked" | 在浏览器扩展管理页面选择"加载已解压的扩展程序" 84 | 4. Select the extracted folder to install | 选择解压后的文件夹即可安装 85 | 86 | --- 87 | 88 | ### 📝 Changes Since ${{ steps.gitinfo.outputs.previous_tag }} | 更新内容 89 | ${{ steps.gitinfo.outputs.commits }} 90 | 91 | --- 92 | 93 | ✅ **Build Info | 构建信息** 94 | - Current Commit: ${{ steps.gitinfo.outputs.sha_short }} 95 | - Build Time: ${{ steps.gitinfo.outputs.build_time }} 96 | 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | 100 | - name: Upload build artifacts 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: bark-sender-extensions-${{ steps.gitinfo.outputs.sha_short }} 104 | path: .output/*.zip 105 | retention-days: 30 106 | include-hidden-files: true 107 | 108 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender Extension/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // Bark Sender Extension 4 | // 5 | // Created by FEI on 11/11/2025. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | import AppKit 11 | 12 | let SFExtensionMessageKey = "message" 13 | 14 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 15 | 16 | func beginRequest(with context: NSExtensionContext) { 17 | let item = context.inputItems[0] as! NSExtensionItem 18 | let message = item.userInfo?[SFExtensionMessageKey] 19 | 20 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 21 | 22 | // 处理来自扩展的消息 23 | if let messageDict = message as? [String: Any], 24 | let action = messageDict["action"] as? String { 25 | 26 | switch action { 27 | case "readClipboard": 28 | handleReadClipboard(context: context) 29 | return 30 | case "ping": 31 | handlePing(context: context) 32 | return 33 | default: 34 | break 35 | } 36 | } 37 | 38 | // 默认响应 39 | let response = NSExtensionItem() 40 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 41 | context.completeRequest(returningItems: [response], completionHandler: nil) 42 | } 43 | 44 | private func handleReadClipboard(context: NSExtensionContext) { 45 | let pasteboard = NSPasteboard.general 46 | var responseData: [String: Any] = [ 47 | "success": false, 48 | "data": "", 49 | "type": "unknown" 50 | ] 51 | 52 | // 检查剪切板中是否有文本内容 53 | if let clipboardText = pasteboard.string(forType: .string) { 54 | responseData = [ 55 | "success": true, 56 | "data": clipboardText, 57 | "type": "text" 58 | ] 59 | os_log(.default, "Successfully read clipboard text: %@", clipboardText) 60 | } else { 61 | // 检查是否有其他类型的内容 62 | let types = pasteboard.types ?? [] 63 | if types.contains(.fileURL) { 64 | responseData = [ 65 | "success": false, 66 | "data": "Clipboard contains file(s), not supported", 67 | "type": "file" 68 | ] 69 | } else if types.contains(.tiff) || types.contains(.png) { 70 | responseData = [ 71 | "success": false, 72 | "data": "Clipboard contains image, not supported", 73 | "type": "image" 74 | ] 75 | } else { 76 | responseData = [ 77 | "success": false, 78 | "data": "Clipboard is empty or contains unsupported content", 79 | "type": "empty" 80 | ] 81 | } 82 | os_log(.default, "Clipboard read failed or unsupported content type") 83 | } 84 | 85 | let response = NSExtensionItem() 86 | response.userInfo = [ SFExtensionMessageKey: responseData ] 87 | context.completeRequest(returningItems: [response], completionHandler: nil) 88 | } 89 | 90 | private func handlePing(context: NSExtensionContext) { 91 | os_log(.default, "Received ping from extension") 92 | 93 | let responseData: [String: Any] = [ 94 | "success": true, 95 | "message": "pong", 96 | "timestamp": Date().timeIntervalSince1970 97 | ] 98 | 99 | let response = NSExtensionItem() 100 | response.userInfo = [ SFExtensionMessageKey: responseData ] 101 | context.completeRequest(returningItems: [response], completionHandler: nil) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /entrypoints/popup/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --u-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E %3Cpath fill='%23ffffff' d='M143.2 611.5c-11.2-22.8-22.3-45.7-33.5-68.5l-44.2-90.1c-6.8-13.9-2.6-24.7 11.9-30.2l63.1-23.7c1.3-.5 2.6-1.5 4-1v213.4l-1.3.1z' /%3E %3Cpath fill='%23ffffff' d='M172.3 511.5v-128c0-4.2-.1-8.3 1.1-12.4a23.1 23.1 0 0128.5-15.6c43.8 12.4 87.7 24.9 131.4 37.5 10.4 3 16.7 11.6 16.7 22.4q-.3 93.8-.8 187.4c0 10.8-5.9 18.8-16.1 22.2q-65.4 21.6-130.8 43c-16.4 5.3-31-6-31-23.6.1-25.9.3-51.9.4-77.9z' /%3E %3Cpath fill='%23ffffff' d='M647 439.1v148.4H377V439.1h270z' /%3E %3Cpath fill='%23ffffff' d='M851.6 513v129.4c0 3.9 0 7.7-1.1 11.4-3.7 11.9-16 19.1-28.1 15.7q-66.1-18.6-132.4-37.7A22.4 22.4 0 01674 610q.2-94.1.7-187.9c.1-11.2 6.1-18.9 17.2-22.5q64.4-21.3 128.9-42.4c17.1-5.6 31.9 5.3 31.8 23.3v42z' /%3E %3Cpath fill='%23ffffff' d='M879.5 627.1V413.5l1.1-.2c1.6 3.2 3.1 6.5 4.7 9.7l73.1 149.1c6.9 14.1 2.5 24.9-12.2 30.5l-61.6 23.1c-1.7.6-3.3 1.6-5.1 1.4z' /%3E%3C/svg%3E"); 3 | --u-icon-bg: linear-gradient(45deg, #6120d1, #5b29d6 11%, #5433db 22%, #4c3ee0 33%, #4449e6 44%, #3c54ec 55%, #355ff1 66%, #2f68f5 77%, #2a6ef9 88%, #2871fa); 4 | --u-icon-bg-color: #fe3b30; 5 | } 6 | 7 | /* 重置和基础样式 */ 8 | html, 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | overflow: hidden; 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 14 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 15 | sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | html { 21 | overflow: hidden; 22 | /* min-height: 0px; */ 23 | /* height: 100%; */ 24 | } 25 | 26 | body { 27 | position: relative; 28 | width: 380px; 29 | min-width: 380px; 30 | /* min-height: 200px; */ 31 | } 32 | 33 | /* 极速模式样式 */ 34 | body.speed-mode { 35 | width: 380px; 36 | min-width: 200px; 37 | min-height: 200px; 38 | height: 200px; 39 | } 40 | 41 | html.u-full { 42 | min-height: unset; 43 | width: 100%; 44 | height: 100%; 45 | } 46 | 47 | html.u-full body { 48 | width: 100%; 49 | } 50 | 51 | #root { 52 | width: 100%; 53 | height: 100%; 54 | overflow: hidden; 55 | } 56 | 57 | /* 滚动条美化 */ 58 | ::-webkit-scrollbar { 59 | width: 6px; 60 | } 61 | 62 | ::-webkit-scrollbar-track { 63 | background: #f1f1f1; 64 | } 65 | 66 | ::-webkit-scrollbar-thumb { 67 | background: #c1c1c1; 68 | border-radius: 3px; 69 | } 70 | 71 | ::-webkit-scrollbar-thumb:hover { 72 | background: #a1a1a1; 73 | } 74 | 75 | /* 确保扩展内容不会超出边界 */ 76 | * { 77 | box-sizing: border-box; 78 | } 79 | 80 | * { 81 | outline: none; 82 | } 83 | 84 | /* 为键盘按键导航 保留 focus-visible 样式 */ 85 | *:focus-visible { 86 | outline: 2px solid #1976d2; 87 | outline-offset: 2px; 88 | } 89 | 90 | /* 基础样式 */ 91 | body { 92 | margin: 0; 93 | padding: 0; 94 | overflow: hidden; 95 | } 96 | 97 | /* 滚动条样式 - 浅色主题 */ 98 | * { 99 | scrollbar-width: thin; 100 | scrollbar-color: #c1c1c1 #f1f1f1; 101 | } 102 | 103 | *::-webkit-scrollbar { 104 | width: 8px; 105 | height: 8px; 106 | } 107 | 108 | *::-webkit-scrollbar-track { 109 | background-color: #f1f1f1; 110 | border-radius: 4px; 111 | } 112 | 113 | *::-webkit-scrollbar-thumb { 114 | background-color: #c1c1c1; 115 | border-radius: 4px; 116 | } 117 | 118 | *::-webkit-scrollbar-thumb:hover { 119 | background-color: #a8a8a8; 120 | } 121 | 122 | *::-webkit-scrollbar-corner { 123 | background-color: #f1f1f1; 124 | } 125 | 126 | /* 滚动条样式 - 深色主题 */ 127 | [data-theme="dark"] * { 128 | scrollbar-color: #555 #2b2b2b; 129 | } 130 | 131 | [data-theme="dark"] *::-webkit-scrollbar-track { 132 | background-color: #2b2b2b; 133 | } 134 | 135 | [data-theme="dark"] *::-webkit-scrollbar-thumb { 136 | background-color: #555; 137 | } 138 | 139 | [data-theme="dark"] *::-webkit-scrollbar-thumb:hover { 140 | background-color: #777; 141 | } 142 | 143 | [data-theme="dark"] *::-webkit-scrollbar-corner { 144 | background-color: #2b2b2b; 145 | } -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Resources/Style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -webkit-user-drag: none; 4 | cursor: default; 5 | box-sizing: border-box; 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | :root { 11 | color-scheme: light dark; 12 | --spacing: 24px; 13 | --border-radius: 12px; 14 | --primary-color: #15181b; 15 | --primary-hover: #0056CC; 16 | --success-color: #34C759; 17 | --warning-color: #FF9500; 18 | --danger-color: #FF3B30; 19 | --text-primary: #1D1D1F; 20 | --text-secondary: #6E6E73; 21 | --background: #FFFFFF; 22 | --surface: #F2F2F7; 23 | --border: #D1D1D6; 24 | --shadow: rgba(0, 0, 0, 0.1); 25 | } 26 | 27 | @media (prefers-color-scheme: dark) { 28 | :root { 29 | --text-primary: #F2F2F7; 30 | --text-secondary: #8E8E93; 31 | --background: #000000; 32 | --surface: #1C1C1E; 33 | --border: #38383A; 34 | --shadow: rgba(0, 0, 0, 0.3); 35 | } 36 | } 37 | 38 | html { 39 | height: 100%; 40 | } 41 | 42 | body { 43 | display: flex; 44 | align-items: center; 45 | justify-content: flex-start; 46 | flex-direction: column; 47 | gap: var(--spacing); 48 | margin: 0; 49 | height: 100%; 50 | min-height: 100vh; 51 | padding: 12px; 52 | font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', system-ui, sans-serif; 53 | font-size: 17px; 54 | line-height: 1.47; 55 | color: var(--text-primary); 56 | background: var(--background); 57 | text-align: center; 58 | } 59 | 60 | /* 状态控制 */ 61 | body:not(.state-on, .state-off) :is(.state-on, .state-off) { 62 | display: none; 63 | } 64 | 65 | body.state-on :is(.state-off, .state-unknown) { 66 | display: none; 67 | } 68 | 69 | body.state-off :is(.state-on, .state-unknown) { 70 | display: none; 71 | } 72 | 73 | /* Header 容器样式 */ 74 | .header-container { 75 | display: flex; 76 | align-items: center; 77 | gap: 20px; 78 | padding: 12px 21px; 79 | margin-top: 15px; 80 | } 81 | 82 | /* Logo 样式 */ 83 | .logo svg { 84 | width: 72px; 85 | height: 72px; 86 | border-radius: 16px; 87 | background: linear-gradient(45deg, #6120d1, #4c3ee0, #2871fa); 88 | box-shadow: 0 4px 16px var(--shadow); 89 | } 90 | 91 | .header { 92 | text-align: left; 93 | } 94 | 95 | h1 { 96 | margin: 0; 97 | font-size: 28px; 98 | font-weight: 700; 99 | color: var(--text-primary); 100 | letter-spacing: -0.02em; 101 | } 102 | 103 | .subtitle { 104 | margin: 0; 105 | font-size: 15px; 106 | font-weight: 400; 107 | color: var(--text-secondary); 108 | line-height: 1.4; 109 | } 110 | 111 | /* 状态消息 */ 112 | .status-container { 113 | width: 100%; 114 | max-width: 320px; 115 | } 116 | 117 | .status-message { 118 | margin: 0; 119 | padding: 16px; 120 | border-radius: var(--border-radius); 121 | font-size: 14px; 122 | font-weight: 500; 123 | line-height: 1.4; 124 | } 125 | 126 | .state-unknown .status-message { 127 | background: var(--surface); 128 | color: var(--text-secondary); 129 | border: 1px solid var(--border); 130 | } 131 | 132 | .state-on .status-message { 133 | background: rgba(52, 199, 89, 0.1); 134 | color: var(--success-color); 135 | border: 1px solid rgba(52, 199, 89, 0.2); 136 | } 137 | 138 | .state-off .status-message { 139 | background: rgba(255, 149, 0, 0.1); 140 | color: var(--warning-color); 141 | border: 1px solid rgba(255, 149, 0, 0.2); 142 | } 143 | 144 | /* Button style */ 145 | button { 146 | border: none; 147 | border-radius: 12px; 148 | padding: 12px 27px; 149 | width: 100%; 150 | max-width: 320px; 151 | font-size: 16px; 152 | text-transform: uppercase; 153 | cursor: pointer; 154 | color: white; 155 | background-color: #1976d2; 156 | outline: none; 157 | position: relative; 158 | overflow: hidden; 159 | } 160 | 161 | button:hover { 162 | background-color: #1075db; 163 | } 164 | 165 | button:active { 166 | background-color: #1976d2; 167 | opacity: 0.9; 168 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/PingButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import IconButton from "@mui/material/IconButton"; 3 | import dayjs from "dayjs"; 4 | // import { useSnackbar, SnackbarKey } from "notistack"; 5 | import NetworkPingIcon from '@mui/icons-material/NetworkPing'; 6 | // import CloseIcon from '@mui/icons-material/Close'; 7 | import { Tooltip, Alert } from "@mui/material"; 8 | import { useTranslation } from "react-i18next"; 9 | 10 | interface PingButtonProps { 11 | apiURL: string; 12 | showAlert: (severity: "success" | "error", message: string) => void; 13 | } 14 | 15 | const PingButton: React.FC = ({ apiURL, showAlert }) => { 16 | // const { enqueueSnackbar, closeSnackbar } = useSnackbar(); 17 | const { t } = useTranslation(); 18 | const [loading, setLoading] = useState(false); 19 | 20 | // const showAlert = ( 21 | // severity: "success" | "error", 22 | // message: string 23 | // ) => { 24 | // enqueueSnackbar("", { 25 | // autoHideDuration: 3000, 26 | // anchorOrigin: { vertical: 'top', horizontal: 'right' }, 27 | // content: (key: SnackbarKey) => ( 28 | // closeSnackbar(key)} 36 | // > 37 | // 38 | // 39 | // } 40 | // > 41 | // {message} 42 | // 43 | // ), 44 | // }); 45 | // }; 46 | 47 | const handlePing = async () => { 48 | const pingURL = new URL(apiURL).origin + "/ping"; 49 | const startTime = dayjs(); 50 | setLoading(true); 51 | 52 | const controller = new AbortController(); 53 | const timeoutId = setTimeout(() => { 54 | controller.abort(); 55 | }, 10000); // 10s 超时 56 | 57 | try { 58 | const response = await fetch(pingURL, { 59 | method: "GET", 60 | signal: controller.signal, 61 | }); 62 | 63 | clearTimeout(timeoutId); 64 | 65 | const data = await response.json(); 66 | const endTime = dayjs(); 67 | const latency = endTime.diff(startTime, "millisecond"); 68 | 69 | if (data.code?.toString().startsWith("2")) { 70 | showAlert( 71 | "success", 72 | `${data.message?.toUpperCase() || t("common.success")} - ${t("common.delay")}: ${latency}ms` 73 | ); 74 | } else { 75 | showAlert( 76 | "error", 77 | `[${data.code}] ${data.message || t("common.failed")}` 78 | ); 79 | } 80 | } catch (error: any) { 81 | if (error.name === "AbortError") { 82 | showAlert("error", t("common.timeout") || "Request Timeout (10s)"); 83 | } else { 84 | showAlert( 85 | "error", 86 | `${t("common.failed")} ${error.message || t("common.error_network")}` 87 | ); 88 | } 89 | } finally { 90 | clearTimeout(timeoutId); 91 | setLoading(false); 92 | } 93 | }; 94 | 95 | return ( 96 | 97 | 98 | 104 | 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | export default PingButton; 112 | -------------------------------------------------------------------------------- /safari_extensions/Bark Sender/Bark Sender/Resources/Script.js: -------------------------------------------------------------------------------- 1 | const translations = { 2 | "en": { 3 | "state_on_modern": "Bark Sender's extension is currently on. You can turn it off in the Extensions section of Safari Settings.", 4 | "state_off_modern": "Bark Sender's extension is currently off. You can turn it on in the Extensions section of Safari Settings.", 5 | "state_unknown_modern": "You can turn on Bark Sender's extension in the Extensions section of Safari Settings.", 6 | "button_modern": "Enable for Safari", 7 | 8 | "state_on_legacy": "Bark Sender's extension is currently on. You can turn it off in Safari Preferences.", 9 | "state_off_legacy": "Bark Sender's extension is currently off. You can turn it on in Safari Preferences.", 10 | "state_unknown_legacy": "You can turn on Bark Sender's extension in Safari Preferences.", 11 | "button_legacy": "Enable for Safari", 12 | 13 | "app_title": "Bark Sender", 14 | "app_subtitle": "Push web content to your iPhone" 15 | }, 16 | "zh_hans": { 17 | "state_on_modern": "Bark Sender 扩展当前已开启。您可以在 Safari 设置的扩展部分中关闭它。", 18 | "state_off_modern": "Bark Sender 扩展当前已关闭。您可以在 Safari 设置的扩展部分中开启它。", 19 | "state_unknown_modern": "您可以在 Safari 设置的扩展部分中开启 Bark Sender 扩展。", 20 | "button_modern": "打开 Safari 设置…", 21 | 22 | "state_on_legacy": "Bark Sender 扩展当前已开启。您可以在 Safari 偏好设置中关闭它。", 23 | "state_off_legacy": "Bark Sender 扩展当前已关闭。您可以在 Safari 偏好设置中开启它。", 24 | "state_unknown_legacy": "您可以在 Safari 扩展偏好设置中开启 Bark Sender 扩展。", 25 | "button_legacy": "打开 Safari 扩展偏好设置", 26 | 27 | "app_title": "Bark Sender", 28 | "app_subtitle": "将网页内容推送到您的 iPhone" 29 | }, 30 | "zh_hant": { 31 | "state_on_modern": "Bark Sender 擴展目前已開啟。您可以在 Safari 設定的延伸功能部分中關閉它。", 32 | "state_off_modern": "Bark Sender 擴展目前已關閉。您可以在 Safari 設定的延伸功能部分中開啟它。", 33 | "state_unknown_modern": "您可以在 Safari 設定的延伸功能部分中開啟 Bark Sender 擴展。", 34 | "button_modern": "開啟 Safari 設定…", 35 | 36 | "state_on_legacy": "Bark Sender 擴展目前已開啟。您可以在 Safari 偏好設定中關閉它。", 37 | "state_off_legacy": "Bark Sender 擴展目前已關閉。您可以在 Safari 偏好設定中開啟它。", 38 | "state_unknown_legacy": "您可以在 Safari 擴展偏好設定中開啟 Bark Sender 擴展。", 39 | "button_legacy": "開啟 Safari 擴展偏好設定", 40 | 41 | "app_title": "Bark Sender", 42 | "app_subtitle": "將網頁內容推送到您的 iPhone" 43 | } 44 | }; 45 | 46 | function getUserLanguage() { 47 | const lang = navigator.language || navigator.userLanguage || 'en'; 48 | 49 | if (lang.startsWith('zh-CN') || lang.startsWith('zh-Hans')) { 50 | return 'zh_hans'; 51 | } else if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK') || lang.startsWith('zh-Hant')) { 52 | return 'zh_hant'; 53 | } else { 54 | return 'en'; 55 | } 56 | } 57 | 58 | function t(key) { 59 | const currentLang = getUserLanguage(); 60 | return translations[currentLang][key] || translations['en'][key] || key; 61 | } 62 | 63 | function show(enabled, useSettingsInsteadOfPreferences) { 64 | const suffix = useSettingsInsteadOfPreferences ? '_modern' : '_legacy'; // macOS 13+ 65 | 66 | document.getElementsByClassName('state-on')[0].innerText = t('state_on' + suffix); 67 | document.getElementsByClassName('state-off')[0].innerText = t('state_off' + suffix); 68 | document.getElementsByClassName('state-unknown')[0].innerText = t('state_unknown' + suffix); 69 | document.getElementsByClassName('open-preferences')[0].innerText = t('button' + suffix); 70 | 71 | const titleElement = document.querySelector('h1'); 72 | const subtitleElement = document.querySelector('.subtitle'); 73 | if (titleElement) titleElement.innerText = t('app_title'); 74 | if (subtitleElement) subtitleElement.innerText = t('app_subtitle'); 75 | 76 | if (typeof enabled === "boolean") { 77 | document.body.classList.toggle(`state-on`, enabled); 78 | document.body.classList.toggle(`state-off`, !enabled); 79 | } else { 80 | document.body.classList.remove(`state-on`); 81 | document.body.classList.remove(`state-off`); 82 | } 83 | } 84 | 85 | function openPreferences() { 86 | webkit.messageHandlers.controller.postMessage("open-preferences"); 87 | } 88 | 89 | document.querySelector("button.open-preferences").addEventListener("click", openPreferences); 90 | -------------------------------------------------------------------------------- /entrypoints/popup/utils/backup-crypto.ts: -------------------------------------------------------------------------------- 1 | export interface EncryptionResult { 2 | encryptedData: string; 3 | iv: string; 4 | salt: string; 5 | } 6 | 7 | export class BackupCrypto { 8 | /** 9 | * 加密备份数据 10 | * @param data 要加密的数据对象 11 | * @param password 用户密码 12 | * @returns 加密结果 13 | */ 14 | static async encrypt(data: any, password: string): Promise { 15 | const encoder = new TextEncoder(); 16 | const decoder = new TextDecoder(); 17 | 18 | // 生成随机盐值和IV 19 | const salt = crypto.getRandomValues(new Uint8Array(16)); 20 | const iv = crypto.getRandomValues(new Uint8Array(12)); 21 | 22 | // 使用 PBKDF2 派生密钥 23 | const keyMaterial = await crypto.subtle.importKey( 24 | 'raw', 25 | encoder.encode(password), 26 | 'PBKDF2', 27 | false, 28 | ['deriveBits', 'deriveKey'] 29 | ); 30 | 31 | const key = await crypto.subtle.deriveKey( 32 | { 33 | name: 'PBKDF2', 34 | salt: salt, 35 | iterations: 100000, 36 | hash: 'SHA-256' 37 | }, 38 | keyMaterial, 39 | { name: 'AES-GCM', length: 256 }, 40 | false, 41 | ['encrypt'] 42 | ); 43 | 44 | // 加密数据 45 | const dataString = JSON.stringify(data); 46 | const encryptedBuffer = await crypto.subtle.encrypt( 47 | { 48 | name: 'AES-GCM', 49 | iv: iv 50 | }, 51 | key, 52 | encoder.encode(dataString) 53 | ); 54 | 55 | // 转换 base64 56 | const encryptedArray = new Uint8Array(encryptedBuffer); 57 | const encryptedData = btoa(String.fromCharCode(...encryptedArray)); 58 | const ivBase64 = btoa(String.fromCharCode(...iv)); 59 | const saltBase64 = btoa(String.fromCharCode(...salt)); 60 | 61 | return { 62 | encryptedData, 63 | iv: ivBase64, 64 | salt: saltBase64 65 | }; 66 | } 67 | 68 | /** 69 | * 解密备份数据 70 | * @param encryptedData 加密的数据 71 | * @param ivBase64 Base64 IV 72 | * @param saltBase64 Base64 Salt 73 | * @param password 用户密码 74 | * @returns 解密后的数据对象 75 | */ 76 | static async decrypt(encryptedData: string, ivBase64: string, saltBase64: string, password: string): Promise { 77 | const encoder = new TextEncoder(); 78 | const decoder = new TextDecoder(); 79 | 80 | // 解码 base64 数据 81 | const salt = new Uint8Array(atob(saltBase64).split('').map(c => c.charCodeAt(0))); 82 | const iv = new Uint8Array(atob(ivBase64).split('').map(c => c.charCodeAt(0))); 83 | const encrypted = new Uint8Array(atob(encryptedData).split('').map(c => c.charCodeAt(0))); 84 | 85 | // 使用相同的 PBKDF2 参数派生密钥 86 | const keyMaterial = await crypto.subtle.importKey( 87 | 'raw', 88 | encoder.encode(password), 89 | 'PBKDF2', 90 | false, 91 | ['deriveBits', 'deriveKey'] 92 | ); 93 | 94 | const key = await crypto.subtle.deriveKey( 95 | { 96 | name: 'PBKDF2', 97 | salt: salt, 98 | iterations: 100000, 99 | hash: 'SHA-256' 100 | }, 101 | keyMaterial, 102 | { name: 'AES-GCM', length: 256 }, 103 | false, 104 | ['decrypt'] 105 | ); 106 | 107 | // 解密数据 108 | const decryptedBuffer = await crypto.subtle.decrypt( 109 | { 110 | name: 'AES-GCM', 111 | iv: iv 112 | }, 113 | key, 114 | encrypted 115 | ); 116 | 117 | const decryptedString = decoder.decode(decryptedBuffer); 118 | return JSON.parse(decryptedString); 119 | } 120 | 121 | /** 122 | * 获取 Chrome 扩展 ID 123 | */ 124 | static getRunId(): string { 125 | try { 126 | return (window as any)?.chrome?.runtime?.id || 'unknown'; 127 | } catch { 128 | return 'unknown'; 129 | } 130 | } 131 | 132 | /** 133 | * 获取扩展版本 134 | */ 135 | static getVersion(): string { 136 | try { 137 | return (window as any)?.chrome?.runtime?.getManifest?.()?.version || '1.0.0'; 138 | } catch { 139 | return '1.0.0'; 140 | } 141 | } 142 | 143 | /** 144 | * 获取 UA 145 | */ 146 | static getUserAgent(): string { 147 | return navigator.userAgent; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /entrypoints/popup/contexts/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useMemo, useState, useEffect, useCallback } from 'react'; 2 | import { AppContextType, AppContextState, AppSettings, EncryptionConfig } from '../types'; 3 | import { detectPlatform, isAppleDevice, getShortcutKeys } from '../utils/platform'; 4 | import { getAppSettings, updateAppSetting as updateAppSettingUtil, saveAppSettings } from '../utils/settings'; 5 | import { cleanupFaviconBlobs } from '../utils/favicon-manager'; 6 | 7 | // 创建Context 8 | const AppContext = createContext(undefined); 9 | 10 | // Provider组件属性接口 11 | interface AppProviderProps { 12 | children: React.ReactNode; 13 | } 14 | 15 | // App Context Provider组件 16 | export function AppProvider({ children }: AppProviderProps) { 17 | const [appSettings, setAppSettings] = useState(null); 18 | const [loading, setLoading] = useState(true); 19 | 20 | // 清理 blob URLs 21 | const cleanupBlobs = useCallback(() => { 22 | cleanupFaviconBlobs(); 23 | }, []); 24 | 25 | // 加载应用设置 26 | const loadSettings = useCallback(async () => { 27 | try { 28 | setLoading(true); 29 | const settings = await getAppSettings(); 30 | setAppSettings(settings); 31 | } catch (error) { 32 | console.error('加载应用设置失败:', error); 33 | } finally { 34 | setLoading(false); 35 | } 36 | }, []); 37 | 38 | // 更新单个设置项 39 | const updateAppSetting = useCallback(async ( 40 | key: K, 41 | value: AppSettings[K] 42 | ): Promise => { 43 | try { 44 | await updateAppSettingUtil(key, value); 45 | setAppSettings(prev => prev ? { ...prev, [key]: value } : null); 46 | } catch (error) { 47 | console.error('更新设置失败:', error); 48 | throw error; 49 | } 50 | }, []); 51 | 52 | // 切换加密开关 53 | const toggleEncryption = useCallback(async (): Promise => { 54 | if (!appSettings) return; 55 | 56 | try { 57 | const newEnabled = !appSettings.enableEncryption; 58 | await updateAppSetting('enableEncryption', newEnabled); 59 | } catch (error) { 60 | console.error('切换加密设置失败:', error); 61 | throw error; 62 | } 63 | }, [appSettings, updateAppSetting]); 64 | 65 | // 更新加密配置 66 | const updateEncryptionConfig = useCallback(async (config: EncryptionConfig): Promise => { 67 | try { 68 | await updateAppSetting('encryptionConfig', config); 69 | } catch (error) { 70 | console.error('更新加密配置失败:', error); 71 | throw error; 72 | } 73 | }, [updateAppSetting]); 74 | 75 | // 重新加载设置 76 | const reloadSettings = useCallback(async (): Promise => { 77 | await loadSettings(); 78 | }, [loadSettings]); 79 | 80 | // 初始化加载设置 81 | useEffect(() => { 82 | loadSettings(); 83 | }, [loadSettings]); 84 | 85 | // 计算应用状态 86 | const appState: AppContextState = useMemo(() => { 87 | const platform = detectPlatform(); 88 | const isApple = isAppleDevice(platform); 89 | const shortcutKeys = getShortcutKeys(platform); 90 | 91 | return { 92 | platform, 93 | isAppleDevice: isApple, 94 | shortcutKeys, 95 | appSettings, 96 | loading 97 | }; 98 | }, [appSettings, loading]); 99 | 100 | // 计算是否显示加密切换按钮 101 | const shouldShowEncryptionToggle = useMemo(() => { 102 | return !!(appSettings?.encryptionConfig?.key && appSettings.encryptionConfig.key.trim() !== ''); 103 | }, [appSettings?.encryptionConfig?.key]); 104 | 105 | const contextValue: AppContextType = useMemo(() => ({ 106 | ...appState, 107 | toggleEncryption, 108 | updateEncryptionConfig, 109 | updateAppSetting, 110 | reloadSettings, 111 | shouldShowEncryptionToggle, 112 | cleanupBlobs 113 | }), [ 114 | appState, 115 | toggleEncryption, 116 | updateEncryptionConfig, 117 | updateAppSetting, 118 | reloadSettings, 119 | shouldShowEncryptionToggle, 120 | cleanupBlobs 121 | ]); 122 | 123 | return ( 124 | 125 | {children} 126 | 127 | ); 128 | } 129 | 130 | // 自定义Hook用于使用App Context 131 | export function useAppContext(): AppContextType { 132 | const context = useContext(AppContext); 133 | 134 | if (!context) { 135 | throw new Error('useAppContext必须在AppProvider内部使用'); 136 | } 137 | 138 | return context; 139 | } -------------------------------------------------------------------------------- /entrypoints/popup/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ThemeProvider } from '@mui/material/styles'; 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { TabValue } from './types'; 6 | import { useDevices } from './hooks/useStorage'; 7 | import { useTheme } from './hooks/useTheme'; 8 | import { AppProvider, useAppContext } from './contexts/AppContext'; 9 | import { initHistoryService } from './utils/history-service'; 10 | import { createAppTheme } from './theme'; 11 | import Layout from './components/Layout'; 12 | import SendPush from './pages/SendPush'; 13 | import History from './pages/History'; 14 | import Settings from './pages/Settings'; 15 | import SpeedMode from './pages/SpeedMode'; 16 | 17 | import { SnackbarProvider } from "notistack"; 18 | 19 | import './i18n'; 20 | import './App.css'; 21 | 22 | // 主应用组件内容 23 | function AppContent() { 24 | const { i18n } = useTranslation(); 25 | const [currentTab, setCurrentTab] = useState('send'); 26 | const { 27 | devices, 28 | defaultDeviceId, 29 | loading: devicesLoading, 30 | addDevice, 31 | editDevice, 32 | removeDevice, 33 | setDefaultDevice, 34 | getDefaultDevice 35 | } = useDevices(); 36 | 37 | const { 38 | themeMode, 39 | effectiveTheme, 40 | updateThemeMode 41 | } = useTheme(); 42 | 43 | const { 44 | appSettings, 45 | toggleEncryption, 46 | shouldShowEncryptionToggle, 47 | reloadSettings, 48 | updateAppSetting 49 | } = useAppContext(); 50 | 51 | // 初始化历史服务 52 | useEffect(() => { 53 | initHistoryService(); 54 | }, []); 55 | const [windowMode,] = useState(new URLSearchParams(window.location.search).get('mode') === 'window'); 56 | // 检查是否是窗口模式并添加类名 57 | useEffect(() => { 58 | if (windowMode) { 59 | document.documentElement.classList.add('u-full'); 60 | } 61 | return () => { 62 | if (windowMode) { 63 | document.documentElement.classList.remove('u-full'); 64 | } 65 | }; 66 | }, [windowMode]); 67 | 68 | // 设置data-theme属性以控制CSS样式 69 | useEffect(() => { 70 | document.documentElement.setAttribute('data-theme', effectiveTheme); 71 | }, [effectiveTheme]); 72 | 73 | if (devicesLoading) { 74 | return
75 |
; 76 | } 77 | // 创建动态主题 78 | const theme = createAppTheme(effectiveTheme, i18n.language?.startsWith('zh') ? 'zh' : 'en'); 79 | 80 | // 退出极速模式 81 | const handleExitSpeedMode = async () => { 82 | await updateAppSetting('enableSpeedMode', false); 83 | // window.close(); 84 | }; 85 | 86 | // 渲染当前页面内容 87 | const renderCurrentPage = () => { 88 | switch (currentTab) { 89 | case 'send': 90 | return ( 91 | 96 | ); 97 | case 'history': 98 | return ; 99 | case 'settings': 100 | return ( 101 | 112 | ); 113 | default: 114 | return ( 115 | 120 | ); 121 | } 122 | }; 123 | 124 | return ( 125 | 126 | 127 | {(!windowMode && appSettings?.enableSpeedMode) ? 128 | 132 | : 133 | 140 | {renderCurrentPage()} 141 | 142 | } 143 | 144 | ); 145 | } 146 | 147 | function App() { 148 | return ( 149 | 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | 157 | export default App; 158 | -------------------------------------------------------------------------------- /entrypoints/popup/hooks/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Device, AppSettings } from '../types'; 3 | import { getDevices, getDefaultDevice, addDevice, removeDevice, setDefaultDevice } from '../utils/storage'; 4 | import { getAppSettings, saveAppSettings } from '../utils/settings'; 5 | 6 | export function useDevices() { 7 | const [devices, setDevices] = useState([]); 8 | const [defaultDeviceId, setDefaultDeviceId] = useState(''); 9 | const [loading, setLoading] = useState(true); 10 | 11 | // 加载设备数据 12 | const loadDevices = async () => { 13 | try { 14 | setLoading(true); 15 | const [deviceList, defaultId] = await Promise.all([ 16 | getDevices(), 17 | getDefaultDevice() 18 | ]); 19 | setDevices(deviceList); 20 | // console.debug('deviceList', deviceList); 21 | setDefaultDeviceId(defaultId); 22 | } catch (error) { 23 | console.error('加载设备数据失败:', error); 24 | } finally { 25 | setLoading(false); 26 | } 27 | }; 28 | 29 | // 添加设备 30 | const handleAddDevice = async ( 31 | alias: string, 32 | apiURL: string, 33 | authorization?: { type: 'basic'; user: string; pwd: string; value: string; }): 34 | Promise => { 35 | const newDevice = await addDevice(alias, apiURL, authorization); 36 | setDevices(prev => [...prev, newDevice]); 37 | 38 | // 如果是第一个设备,设为默认设备 39 | if (devices.length === 0) { 40 | await handleSetDefaultDevice(newDevice.id); 41 | } 42 | 43 | return newDevice; 44 | }; 45 | 46 | // 删除设备 47 | const handleRemoveDevice = async (deviceId: string) => { 48 | await removeDevice(deviceId); 49 | setDevices(prev => prev.filter(device => device.id !== deviceId)); 50 | 51 | // 如果删除的是默认设备,更新默认设备状态 52 | if (defaultDeviceId === deviceId) { 53 | const remainingDevices = devices.filter(device => device.id !== deviceId); 54 | if (remainingDevices.length > 0) { 55 | // 按照id倒序寻找最后一个设备作为新的默认设备 56 | const lastDevice = [...remainingDevices].sort((a, b) => b.id.localeCompare(a.id))[0]; 57 | await handleSetDefaultDevice(lastDevice.id); 58 | } else { 59 | setDefaultDeviceId(''); 60 | } 61 | } 62 | }; 63 | 64 | // 编辑设备 65 | const handleEditDevice = async (oldDeviceId: string, alias: string, apiURL: string, authorization?: { type: 'basic'; user: string; pwd: string; value: string; }): Promise => { 66 | const isDefault = defaultDeviceId === oldDeviceId; 67 | const newDevice = await addDevice(alias, apiURL, authorization); 68 | 69 | // 更新设备列表 70 | setDevices(prev => { 71 | const filtered = prev.filter(device => device.id !== oldDeviceId); 72 | return [...filtered, newDevice]; 73 | }); 74 | 75 | // 如果编辑的是默认设备,保持新设备为默认设备 76 | if (isDefault) { 77 | await handleSetDefaultDevice(newDevice.id); 78 | } 79 | 80 | // 删除旧设备 81 | await removeDevice(oldDeviceId); 82 | 83 | return newDevice; 84 | }; 85 | 86 | // 设置默认设备 87 | const handleSetDefaultDevice = async (deviceId: string) => { 88 | await setDefaultDevice(deviceId); 89 | setDefaultDeviceId(deviceId); 90 | }; 91 | 92 | // 获取默认设备 93 | const getDefaultDeviceInfo = () => { 94 | return devices.find(device => device.id === defaultDeviceId) || null; 95 | }; 96 | 97 | useEffect(() => { 98 | loadDevices(); 99 | }, []); 100 | 101 | return { 102 | devices, 103 | defaultDeviceId, 104 | loading, 105 | addDevice: handleAddDevice, 106 | editDevice: handleEditDevice, 107 | removeDevice: handleRemoveDevice, 108 | setDefaultDevice: handleSetDefaultDevice, 109 | getDefaultDevice: getDefaultDeviceInfo 110 | }; 111 | } 112 | 113 | // 设置管理hook 114 | export function useAppSettings() { 115 | const [settings, setSettings] = useState({ 116 | enableContextMenu: true, 117 | enableInspectSend: true, 118 | themeMode: 'system', 119 | enableEncryption: false, 120 | encryptionConfig: { 121 | algorithm: 'AES256', 122 | mode: 'CBC', 123 | padding: 'pkcs7', 124 | key: '' 125 | }, 126 | enableBasicAuth: false 127 | }); 128 | const [loading, setLoading] = useState(true); 129 | 130 | // 加载设置 131 | const loadSettings = async () => { 132 | try { 133 | setLoading(true); 134 | const appSettings = await getAppSettings(); 135 | setSettings(appSettings); 136 | } catch (error) { 137 | console.error('加载应用设置失败:', error); 138 | } finally { 139 | setLoading(false); 140 | } 141 | }; 142 | 143 | // 更新设置 144 | const updateSettings = async (newSettings: AppSettings) => { 145 | await saveAppSettings(newSettings); 146 | setSettings(newSettings); 147 | }; 148 | 149 | useEffect(() => { 150 | loadSettings(); 151 | }, []); 152 | 153 | return { 154 | settings, 155 | loading, 156 | updateSettings, 157 | reload: loadSettings 158 | }; 159 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/DeviceSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | FormControl, 4 | InputLabel, 5 | Select, 6 | MenuItem, 7 | Box, 8 | Typography, 9 | SelectChangeEvent, 10 | Stack, 11 | Divider 12 | } from '@mui/material'; 13 | import AddIcon from '@mui/icons-material/Add'; 14 | import { useTranslation } from 'react-i18next'; 15 | import { Device } from '../types'; 16 | 17 | interface DeviceSelectProps { 18 | devices: Device[]; 19 | selectedDevice: Device | null; 20 | onDeviceChange: (device: Device | null) => void; 21 | onAddClick: () => void; 22 | label?: string; 23 | placeholder?: string; 24 | showLabel?: boolean; 25 | } 26 | 27 | export default function DeviceSelect({ 28 | devices, 29 | selectedDevice, 30 | onDeviceChange, 31 | onAddClick, 32 | label = '选择设备', 33 | placeholder = '请选择一个设备', 34 | showLabel = true 35 | }: DeviceSelectProps) { 36 | const { t } = useTranslation(); 37 | const [open, setOpen] = useState(false); 38 | 39 | const handleChange = (event: SelectChangeEvent) => { 40 | const deviceId = event.target.value; 41 | const device = devices.find(d => d.id === deviceId) || null; 42 | onDeviceChange(device); 43 | }; 44 | 45 | const handleAddClick = (e: React.MouseEvent) => { 46 | e.stopPropagation(); 47 | setOpen(false); 48 | onAddClick(); 49 | }; 50 | 51 | return ( 52 | 53 | {showLabel && 62 | {/* 目标设备 */} 63 | {t('push.target_device')} 64 | } 65 | 139 | 140 | ); 141 | } -------------------------------------------------------------------------------- /entrypoints/popup/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { Device } from '../types'; 2 | import { formatApiURL } from './api'; 3 | 4 | // 存储键名常量 5 | const STORAGE_KEYS = { 6 | DEVICES: 'bark_devices', 7 | DEFAULT_DEVICE: 'bark_default_device' 8 | }; 9 | 10 | // 获取浏览器存储API - 使用Promise包装以兼容不同API 11 | function getStorage() { 12 | return new Promise((resolve, reject) => { 13 | try { 14 | // 检查chrome.storage是否可用 15 | const chromeAPI = (window as any).chrome; 16 | if (chromeAPI && chromeAPI.storage) { 17 | resolve(chromeAPI.storage); 18 | return; 19 | } 20 | 21 | // 检查browser.storage是否可用 22 | const browserAPI = (window as any).browser; 23 | if (browserAPI && browserAPI.storage) { 24 | resolve(browserAPI.storage); 25 | return; 26 | } 27 | 28 | // 作为开发环境的fallback,使用localStorage模拟 29 | const localStorageMock = { 30 | local: { 31 | get: (keys: string) => { 32 | return Promise.resolve({ 33 | [keys]: JSON.parse(localStorage.getItem(keys) || 'null') 34 | }); 35 | }, 36 | set: (items: Record) => { 37 | Object.keys(items).forEach(key => { 38 | localStorage.setItem(key, JSON.stringify(items[key])); 39 | }); 40 | return Promise.resolve(); 41 | } 42 | } 43 | }; 44 | resolve(localStorageMock); 45 | } catch (error) { 46 | reject(new Error('存储API初始化失败')); 47 | } 48 | }); 49 | } 50 | 51 | // 获取设备列表 52 | export async function getDevices(): Promise { 53 | try { 54 | const storage: any = await getStorage(); 55 | const result = await storage.local.get(STORAGE_KEYS.DEVICES); 56 | return result[STORAGE_KEYS.DEVICES] || []; 57 | } catch (error) { 58 | console.error('获取设备列表失败:', error); 59 | return []; 60 | } 61 | } 62 | 63 | // 保存设备列表 64 | export async function saveDevices(devices: Device[]): Promise { 65 | try { 66 | const storage: any = await getStorage(); 67 | await storage.local.set({ [STORAGE_KEYS.DEVICES]: devices }); 68 | } catch (error) { 69 | console.error('保存设备列表失败:', error); 70 | throw error; 71 | } 72 | } 73 | 74 | // 添加新设备 75 | export async function addDevice(alias: string, apiURL: string, authorization?: { type: 'basic'; user: string; pwd: string; value: string; }): Promise { 76 | const timestamp = Date.now(); 77 | const date = new Date(timestamp); 78 | 79 | // 格式化为 YYYY-MM-DD HH:mm:ss 80 | const year = date.getFullYear(); 81 | const month = String(date.getMonth() + 1).padStart(2, '0'); 82 | const day = String(date.getDate()).padStart(2, '0'); 83 | const hours = String(date.getHours()).padStart(2, '0'); 84 | const minutes = String(date.getMinutes()).padStart(2, '0'); 85 | const seconds = String(date.getSeconds()).padStart(2, '0'); 86 | const createdAt = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 87 | 88 | // 格式化 URL 89 | const formattedApiURL = formatApiURL(apiURL); 90 | 91 | let server: string | undefined; 92 | let deviceKey: string | undefined; 93 | 94 | try { 95 | const url = new URL(formattedApiURL); 96 | // const oldServer = `${url.protocol}//${url.host}`; // 旧的获取 server 的方式,新的应该是整段地址除去最后一段 // 97 | 98 | const pathParts = url.pathname.split('/').filter(part => part); 99 | if (pathParts.length > 0) { 100 | deviceKey = pathParts[pathParts.length - 1]; 101 | server = formattedApiURL.slice(0, -deviceKey.length - 1).replace(/\/$/, ''); // 去除最后的 / 102 | // console.debug('server', server, 'deviceKey', deviceKey, formattedApiURL, 'oldServer', oldServer); 103 | } 104 | } catch (error) { 105 | console.error('解析API URL失败:', error); 106 | } 107 | 108 | const device: Device = { 109 | id: timestamp.toString(), 110 | alias, 111 | apiURL: formattedApiURL, 112 | createdAt, 113 | timestamp, 114 | ...(authorization && { authorization }), // 如果提供了认证信息,则添加到设备中 115 | ...(server && { server }), // 服务器地址 116 | ...(deviceKey && { deviceKey }) // deviceKey 117 | }; 118 | 119 | const devices = await getDevices(); 120 | devices.push(device); 121 | await saveDevices(devices); 122 | 123 | return device; 124 | } 125 | 126 | // 删除设备 127 | export async function removeDevice(deviceId: string): Promise { 128 | const devices = await getDevices(); 129 | const updatedDevices = devices.filter(device => device.id !== deviceId); 130 | await saveDevices(updatedDevices); 131 | 132 | // 如果删除的是默认设备,清除默认设备设置 133 | const defaultDevice = await getDefaultDevice(); 134 | if (defaultDevice === deviceId) { 135 | await setDefaultDevice(''); 136 | } 137 | } 138 | 139 | // 获取默认设备ID 140 | export async function getDefaultDevice(): Promise { 141 | try { 142 | const storage: any = await getStorage(); 143 | const result = await storage.local.get(STORAGE_KEYS.DEFAULT_DEVICE); 144 | return result[STORAGE_KEYS.DEFAULT_DEVICE] || ''; 145 | } catch (error) { 146 | console.error('获取默认设备失败:', error); 147 | return ''; 148 | } 149 | } 150 | 151 | // 设置默认设备 152 | export async function setDefaultDevice(deviceId: string): Promise { 153 | try { 154 | const storage: any = await getStorage(); 155 | await storage.local.set({ [STORAGE_KEYS.DEFAULT_DEVICE]: deviceId }); 156 | } catch (error) { 157 | console.error('设置默认设备失败:', error); 158 | throw error; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /entrypoints/popup/components/LocalSyncCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { BackupData } from './BackupRestoreCard'; 3 | import Paper from '@mui/material/Paper'; 4 | import Typography from '@mui/material/Typography'; 5 | import List from '@mui/material/List'; 6 | import ListItem from '@mui/material/ListItem'; 7 | import ListItemIcon from '@mui/material/ListItemIcon'; 8 | import ListItemText from '@mui/material/ListItemText'; 9 | import ListItemButton from '@mui/material/ListItemButton'; 10 | import FolderIcon from '@mui/icons-material/Folder'; 11 | import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; 12 | import RestoreIcon from '@mui/icons-material/Restore'; 13 | import { Device, ThemeMode } from '../types'; 14 | import BackupDialog from './BackupDialog'; 15 | import RestoreDialog from './RestoreDialog'; 16 | import { useTranslation } from 'react-i18next'; 17 | 18 | interface LocalSyncCardProps { 19 | devices: Device[]; 20 | defaultDeviceId: string; 21 | onSettingsChange?: () => void; 22 | // 设备操作函数 23 | onAddDevice?: (alias: string, apiURL: string, authorization?: { type: 'basic'; user: string; pwd: string; value: string; }) => Promise; 24 | onEditDevice?: (oldDeviceId: string, alias: string, apiURL: string, authorization?: { type: 'basic'; user: string; pwd: string; value: string; }) => Promise; 25 | onSetDefaultDevice?: (deviceId: string) => Promise; 26 | // 主题操作函数 27 | onThemeChange?: (mode: ThemeMode) => void; 28 | isDragging?: boolean; 29 | showToast?: (severity: 'error' | 'warning' | 'info' | 'success', message: string) => void; 30 | } 31 | 32 | export default function LocalSyncCard({ 33 | devices, 34 | defaultDeviceId, 35 | onSettingsChange, 36 | onAddDevice, 37 | onEditDevice, 38 | onSetDefaultDevice, 39 | onThemeChange, 40 | isDragging, 41 | showToast 42 | }: LocalSyncCardProps) { 43 | const [backupDialogOpen, setBackupDialogOpen] = useState(false); 44 | const [restoreDialogOpen, setRestoreDialogOpen] = useState(false); 45 | const [backupData, setBackupData] = useState(null); 46 | const fileInputRef = useRef(null); 47 | const { t } = useTranslation(); 48 | 49 | // 处理文件选择 50 | const handleFileSelect = async (event: React.ChangeEvent) => { 51 | const file = event.target.files?.[0]; 52 | if (!file) { 53 | return; 54 | } 55 | 56 | try { 57 | const fileContent = await file.text(); 58 | const parsedBackupData: BackupData = JSON.parse(fileContent); 59 | 60 | // 验证备份文件格式 61 | if (!parsedBackupData.version || !parsedBackupData.runId || typeof parsedBackupData.encrypted !== 'boolean') { 62 | showToast?.('error', t('backup.restore_dialog.invalid_format')); 63 | return; 64 | } 65 | 66 | setBackupData(parsedBackupData); 67 | setRestoreDialogOpen(true); 68 | } catch (error) { 69 | console.error('文件解析失败:', error); 70 | showToast?.('error', t('backup.restore_dialog.file_parse_failed')); 71 | } 72 | }; 73 | return ( 74 | <> 75 | 76 | 77 | 78 | {/* 本地同步 */} 79 | {t('backup.local')} 80 | 81 | 82 | 83 | 84 | setBackupDialogOpen(true)}> 85 | 86 | 87 | 88 | 92 | 93 | 94 | 95 | 96 | fileInputRef.current?.click()} disabled={isDragging}> 97 | 98 | 99 | 100 | 104 | 105 | 106 | 107 | 108 | 109 | {/* 文件输入 */} 110 | 117 | 118 | {/* 备份对话框 */} 119 | setBackupDialogOpen(false)} 122 | devices={devices} 123 | defaultDeviceId={defaultDeviceId} 124 | /> 125 | 126 | {/* 还原对话框 */} 127 | { 130 | setRestoreDialogOpen(false); 131 | setBackupData(null); 132 | if (fileInputRef.current) { 133 | fileInputRef.current.value = ''; 134 | } 135 | }} 136 | devices={devices} 137 | defaultDeviceId={defaultDeviceId} 138 | onSettingsChange={onSettingsChange} 139 | onAddDevice={onAddDevice} 140 | onEditDevice={onEditDevice} 141 | onSetDefaultDevice={onSetDefaultDevice} 142 | onThemeChange={onThemeChange} 143 | cloudBackupData={backupData} 144 | /> 145 | 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bark Sender 2 | 3 | ### Quick Install | 快速上手 4 | Just click the badge/ link below to install it from your browser's extension store! 5 | 6 | 点击下方 对应浏览器的 徽标/ 链接 即可跳转至的扩展商店进行安装! 7 | 8 | 9 | 10 | 16 | 22 | 28 | 29 | 30 | 33 | 34 |
11 | 12 | Firefox 13 |
14 | For Mozilla Firefox 15 |
17 | 18 | Chrome 19 |
20 | For Google Chrome 21 |
23 | 24 | Edge 25 |
26 | For Microsoft Edge 27 |
31 | For Safari (Mac) 32 |
35 | 36 | --- 37 | 38 | [🇺🇸 English](#english-description) | [🇨🇳 中文说明](#中文说明) 39 | 40 | ## English Description 41 | 42 | **Click the badges above to install the extension from your preferred store.** 43 | 44 | Bark Sender is a browser extension that allows you to quickly push selected text from a webpage or clipboard content to any iOS device with the [ Bark App ](https://apps.apple.com/app/bark-custom-notifications/id1403753865) installed. 45 | 46 | 🧩 Features: 47 | - Select any text on a webpage and right-click to send it; 48 | - Right-click on any page to send the current URL; 49 | - Use a keyboard shortcut to send the current clipboard content; 50 | - Supports adding multiple iOS devices with Bark App installed. 51 | 52 | 📌 Requirements: 53 | You must install the [ Bark App ](https://apps.apple.com/app/bark-custom-notifications/id1403753865) on your iOS device and enable notification permissions. 54 | 55 | 📱 How to Add iOS Devices: 56 | - Open the Bark App on your iOS device, tap the cloud icon in the top-right corner to open the server list; 57 | - Tap any server and choose "Copy URL and Key"; 58 | - In the extension settings page, add the device using the format: `https://api.day.app/:key/`; 59 | - Select text and right-click to push it to your default device. If no text is selected, right-click will send the current page URL instead. 60 | 61 |  **Safari Setup Guide:** 62 | 1. Open the Bark Sender app, click "ENABLE FOR SAFARI" - this will automatically open Safari's extension settings page; 63 | 2. Check the extension checkbox to enable it; 64 | 3. If the right-click menu doesn't work, it's because "Enable inspect and send web content" was enabled in Settings page. This parsing feature requires additional permissions; 65 | 4. Go to Permissions and find "Web Page Contents and Browsing History", click "Always Allow on Every Website..."; 66 | 5. If you don't need parsing functionality and only want to send page URLs via right-click, you can disable "Enable inspect and send web content"; 67 | 6. **Known Issue:** Some Safari versions have poor clipboard reading support. You may need to wait a while after Safari starts before the feature works properly. For other issues, please [click here to submit an issue](https://github.com/ij369/bark-sender/issues). 68 | 69 | --- 70 | 71 | ## Demo 72 | 73 | https://github.com/user-attachments/assets/4e1cef2b-660d-45f8-ab79-699f6e9696c5 74 | 75 | [https://www.youtube.com/watch?v=0aw8F1Wo-n4](https://www.youtube.com/watch?v=0aw8F1Wo-n4) 76 | 77 | --- 78 | 79 | ## Build Instructions 80 | 81 | 📋 **For Extension Build** 82 | 83 | To build this extension from source code, please refer to the detailed build instructions: 84 | 85 | **👉 [README-BUILD.md](./README-BUILD.md)** 86 | 87 | **Quick Build:** 88 | ```bash 89 | ./build.sh 90 | ``` 91 | 92 | The final extension packages will be generated at: 93 | - Firefox: `.output/bark-sender--firefox.zip` 94 | - Chrome/Edge: `.output/bark-sender--chrome.zip` 95 | 96 | ## Acknowledgements 97 | 98 | The icons in this project are adapted from [Bark](https://github.com/Finb/bark), designed by [Finb](https://github.com/Finb), and were modified and used with the author’s permission granted before the public release on the browser store. 99 | 100 | --- 101 | 102 | ## 中文说明 103 | 104 | **点击上方徽标从对应应用商店安装扩展。** 105 | 106 | Bark Sender 是一个浏览器扩展,允许你将网页中的文字内容或 PC 剪贴板中的文本,快速推送到安装了 [ Bark App ](https://apps.apple.com/app/bark-custom-notifications/id1403753865) 的 iOS 设备上。 107 | 108 | 🧩 本扩展实现以下功能: 109 | 1. 选中网页上的任意文字,右键进行发送; 110 | 2. 在任意页面右键发送当前页面的网址; 111 | 3. 拷贝的任何一段信息,通过快捷键来发送剪切板的内容; 112 | 4. 支持添加多个装有 Bark App 的 iOS 设备。 113 | 114 | 📌 前提要求: 115 | 需要在 iOS 设备上安装 [ Bark App ](https://apps.apple.com/app/bark-custom-notifications/id1403753865) 并开启消息推送权限。 116 | 117 | 📱 如何添加 iOS 设备: 118 | 1. 打开 iOS 设备上的 Bark App,点击右上角的云朵图标,打开服务器列表; 119 | 2. 点击任意服务器,选择“复制地址和 Key”; 120 | 3. 在扩展配置页里添加设备,格式为:`https://api.day.app/:key/`; 121 | 4. 选中文字,右键发送文字;未选中文字时,右键将发送当前页面链接。 122 | 123 |  **Safari 使用说明:** 124 | 1. 打开 Bark Sender APP 软件本体,点击"打开 Safari 设置",会自动打开 Safari 扩展的设置页; 125 | 2. 将扩展复选框打勾启用扩展; 126 | 3. 如果右键菜单无法使用,是因为扩展的配置页里默认启用了"启用右键解析网页内容",解析功能需要额外权限; 127 | 4. 需要在 Permissions 中,找到 "网页内容和浏览历史记录" 一项,点击 "在每个网站上始终允许..."; 128 | 5. 如果不需要解析功能,只需要右键发送页面地址,可以关闭 "启用右键解析网页内容"; 129 | 6. **已知问题:** 部分版本的 Safari 对读取剪切板支持不太好,需要等待 Safari 启动一段时间后才能使用。如有其他问题欢迎[点击这里提 issue](https://github.com/ij369/bark-sender/issues)。 130 | 131 | ## 演示 132 | 133 | [https://www.youtube.com/watch?v=oRxYjg2clbk](https://www.youtube.com/watch?v=oRxYjg2clbk) 134 | 135 | ## 构建说明 136 | 137 | 📋 **扩展构建** 138 | 139 | 要从源代码构建此扩展,请参考详细的构建说明文档: 140 | 141 | **👉 [README-BUILD.md](./README-BUILD.md)** 142 | 143 | **快速构建:** 144 | ```bash 145 | ./build.sh 146 | ``` 147 | 148 | 最终的扩展包将在以下位置生成: 149 | - Firefox: `.output/bark-sender--firefox.zip` 150 | - Chrome/Edge: `.output/bark-sender--chrome.zip` 151 | 152 | ## 致谢 153 | 154 | 本项目的图标基于 [Finb](https://github.com/Finb) 设计的 [Bark](https://github.com/Finb/bark) 二次创作,在浏览器商店公开前已获得原作者修改许可与使用。 155 | -------------------------------------------------------------------------------- /entrypoints/popup/components/FeatureSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Box, 4 | Typography, 5 | Stack, 6 | Paper, 7 | FormControlLabel, 8 | Switch, 9 | Divider, 10 | Chip 11 | } from '@mui/material'; 12 | import TuneIcon from '@mui/icons-material/Tune'; 13 | import { useTranslation } from 'react-i18next'; 14 | import { useAppContext } from '../contexts/AppContext'; 15 | import AvatarSetting from './AvatarSetting'; 16 | import FaviconSetting from './FaviconSetting'; 17 | import SpeedModeSetting from './SpeedModeSetting'; 18 | import { DEFAULT_ADVANCED_PARAMS } from '../utils/settings'; 19 | 20 | interface FeatureSettingsProps { 21 | devices: any[]; 22 | onError: (error: string) => void; 23 | onToast: (message: string) => void; 24 | } 25 | 26 | export default function FeatureSettings({ devices, onError, onToast }: FeatureSettingsProps) { 27 | const { t } = useTranslation(); 28 | const { appSettings, updateAppSetting } = useAppContext(); 29 | 30 | const handleContextMenuToggle = async (enabled: boolean) => { 31 | try { 32 | await updateAppSetting('enableContextMenu', enabled); 33 | } catch (error) { 34 | onError(t('common.error_update', { message: error instanceof Error ? error.message : '未知错误' })); 35 | } 36 | }; 37 | 38 | // 处理右键解析网页内容开关切换 39 | const handleInspectSendToggle = async (enabled: boolean) => { 40 | try { 41 | await updateAppSetting('enableInspectSend', enabled); 42 | } catch (error) { 43 | onError(t('common.error_update', { message: error instanceof Error ? error.message : '未知错误' })); 44 | } 45 | }; 46 | 47 | // 处理完整参数配置开关切换 48 | const handleAdvancedParamsToggle = async (enabled: boolean) => { 49 | try { 50 | await updateAppSetting('enableAdvancedParams', enabled); 51 | 52 | if (enabled) { 53 | onToast(t('settings.advanced_params.success_message')); 54 | } else { 55 | // 关闭时,重置参数配置为默认值 56 | const defaultParamsJson = JSON.stringify(DEFAULT_ADVANCED_PARAMS, null, 2); 57 | await updateAppSetting('advancedParamsJson', defaultParamsJson); 58 | onToast(t('settings.advanced_params.reset_message')); 59 | } 60 | } catch (error) { 61 | onError(t('common.error_update', { message: error instanceof Error ? error.message : '未知错误' })); 62 | } 63 | }; 64 | 65 | // 处理 API v2 开关切换 66 | const handleApiV2Toggle = async (enabled: boolean) => { 67 | try { 68 | await updateAppSetting('enableApiV2', enabled); 69 | } catch (error) { 70 | onError(t('common.error_update', { message: error instanceof Error ? error.message : '未知错误' })); 71 | } 72 | }; 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | {/* 功能设置 */} 80 | {t('settings.features.title')} 81 | 82 | 83 | 84 | {/* 右键菜单 */} 85 | {t('settings.context_menu.title')} 86 | 87 | handleContextMenuToggle(e.target.checked)} 93 | color='primary' 94 | /> 95 | } 96 | label={t('settings.context_menu.enable')} 97 | sx={{ userSelect: 'none' }} 98 | /> 99 | {appSettings?.enableContextMenu && ( 100 | handleInspectSendToggle(e.target.checked)} 106 | color='warning' 107 | /> 108 | } 109 | label={t('settings.context_menu.enable_inspect_send')} 110 | sx={{ userSelect: 'none' }} 111 | /> 112 | )} 113 | 114 | 115 | 116 | 117 | 118 | {/* 自定义头像 */} 119 | 120 | 121 | {/* 网站图标设置 */} 122 | {appSettings?.enableInspectSend && 123 | } 124 | 125 | {/* 启用极速模式 */} 126 | 127 | 128 | {/* 启用完整的参数配置 */} 129 | handleAdvancedParamsToggle(e.target.checked)} 134 | /> 135 | } 136 | // label="启用完整的参数配置" 137 | label={t('settings.advanced_params.enable')} 138 | sx={{ userSelect: 'none' }} 139 | /> 140 | 141 | 142 | {/* API v2 开关 */} 143 | handleApiV2Toggle(e.target.checked)} 148 | /> 149 | } 150 | label={ 151 | {t('settings.api_v2.title')} 152 | {/* */} 153 | } 154 | sx={{ userSelect: 'none' }} 155 | /> 156 | 157 | 158 | 159 | 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /entrypoints/popup/components/PreviewCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Box, 4 | Stack, 5 | Typography, 6 | } from '@mui/material'; 7 | import { useTranslation } from 'react-i18next'; 8 | import dayjs from "dayjs"; 9 | import utc from "dayjs/plugin/utc"; 10 | import timezone from "dayjs/plugin/timezone"; 11 | import { getFaviconUrl } from '../utils/favicon-manager'; 12 | import { useAppContext } from '../contexts/AppContext'; 13 | 14 | // 注册插件 15 | dayjs.extend(utc); 16 | dayjs.extend(timezone); 17 | 18 | interface PreviewCardProps { 19 | parameters: any; 20 | } 21 | 22 | export default function PreviewCard({ parameters, }: PreviewCardProps) { 23 | const { t } = useTranslation(); 24 | const { cleanupBlobs } = useAppContext(); 25 | const [faviconUrl, setFaviconUrl] = useState(parameters.icon || ''); 26 | 27 | // 加载 favicon 28 | useEffect(() => { 29 | let isMounted = true; 30 | 31 | const loadFavicon = async () => { 32 | if (!parameters.icon) { 33 | setFaviconUrl(''); 34 | return; 35 | } 36 | 37 | try { 38 | // 清理之前的 blob URLs 39 | cleanupBlobs(); 40 | 41 | // 获取缓存的 favicon URL 42 | const cachedUrl = await getFaviconUrl(parameters.icon, parameters.sendTimestamp); 43 | 44 | if (isMounted) { 45 | setFaviconUrl(cachedUrl); 46 | } 47 | } catch (error) { 48 | console.error('加载 favicon 失败:', error); 49 | if (isMounted) { 50 | setFaviconUrl(parameters.icon || ''); 51 | } 52 | } 53 | }; 54 | 55 | loadFavicon(); 56 | 57 | return () => { 58 | isMounted = false; 59 | }; 60 | }, [parameters.icon, parameters.sendTimestamp, cleanupBlobs]); 61 | 62 | return ( 63 | 67 | 86 | 87 | 88 | 89 | {t('settings.avatar.preview')} 100 | 101 | {!faviconUrl ? ( 102 | 103 | 112 | 117 | 118 | 119 | 120 | 121 | ) : 122 | ( 123 | 135 | 140 | 141 | 142 | 143 | 144 | ) 145 | } 146 | 147 | 148 | 149 | {parameters.title || 'Bark'} 150 | 151 | 158 | {parameters.body} 159 | 160 | 161 | 162 | 163 | 164 | {dayjs.tz(parameters.sendTimestamp * 1000, "YYYY-MM-DD HH:mm:ss", parameters.timezone).format('HH:mm')} 165 | 166 | 167 | 168 | 169 | ); 170 | } 171 | -------------------------------------------------------------------------------- /entrypoints/popup/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Box, 4 | AppBar, 5 | Toolbar, 6 | Typography, 7 | BottomNavigation, 8 | BottomNavigationAction, 9 | Paper, 10 | Stack, 11 | IconButton, 12 | Tooltip 13 | } from '@mui/material'; 14 | import SendIcon from '@mui/icons-material/Send'; 15 | import HistoryIcon from '@mui/icons-material/History'; 16 | import SettingsIcon from '@mui/icons-material/Settings'; 17 | import LockIcon from '@mui/icons-material/Lock'; 18 | import LockOpenIcon from '@mui/icons-material/LockOpen'; 19 | // import OpenInNewIcon from '@mui/icons-material/OpenInNew'; 20 | import { useTranslation } from 'react-i18next'; 21 | import { TabValue } from '../types'; 22 | import LanguageSelect from './LanguageSelect'; 23 | import { detectPlatform } from '../utils/platform'; 24 | 25 | interface LayoutProps { 26 | children: React.ReactNode; 27 | currentTab: TabValue; 28 | onTabChange: (newTab: TabValue) => void; 29 | // 加密相关 props 30 | showEncryptionToggle?: boolean; 31 | encryptionEnabled?: boolean; 32 | onEncryptionToggle?: () => void; 33 | } 34 | 35 | export default function Layout({ 36 | children, 37 | currentTab, 38 | onTabChange, 39 | showEncryptionToggle = false, 40 | encryptionEnabled = false, 41 | onEncryptionToggle 42 | }: LayoutProps) { 43 | const { t } = useTranslation(); 44 | const [isWindowMode] = useState(new URLSearchParams(window.location.search).get('mode') === 'window'); 45 | 46 | // 打开小窗口 47 | const handleOpenWindow = (event: React.MouseEvent) => { 48 | browser.windows.getCurrent((win) => { // macOS 窗口全屏模式会显示扩展栏,打开的小窗会自动进入全屏状态会很难看,所以不打开小窗 49 | const windowState = win.state || 'normal'; 50 | 51 | if (windowState === 'fullscreen' || // 如果当前浏览器窗口是全屏状态 52 | isWindowMode) { // 如果当前本身就是小窗 53 | return; 54 | } 55 | // 获取鼠标点击位置 56 | const { screenX, } = event; 57 | 58 | // 计算窗口位置,使窗口中心对准鼠标点击位置 59 | const windowWidth = 380; 60 | const windowHeight = 660; 61 | const left = Math.max(0, screenX - windowWidth / 2); 62 | 63 | const platform = detectPlatform(); 64 | browser.windows.create({ 65 | url: browser.runtime.getURL('/popup.html?mode=window'), 66 | type: 'popup', 67 | width: windowWidth, 68 | height: windowHeight, 69 | left: Math.round(left), 70 | top: platform === 'unknown' ? 90 : (platform === 'mac' ? 120 : 90), // 如果是 Windows 则为 90,如果是 Mac 则为 120 71 | focused: true, 72 | }); 73 | window.close(); 74 | }); 75 | }; 76 | const getTabIndex = (tab: TabValue): number => { 77 | const tabs: TabValue[] = ['send', 'history', 'settings']; 78 | return tabs.indexOf(tab); 79 | }; 80 | 81 | const getTabValue = (index: number): TabValue => { 82 | const tabs: TabValue[] = ['send', 'history', 'settings']; 83 | return tabs[index] || 'send'; 84 | }; 85 | 86 | return ( 87 | 98 | {/* 主要内容区域:AppBar + 内容 */} 99 | 106 | {/* 顶部AppBar */} 107 | 108 | 109 | 113 | Bark Sender 114 | 115 | {/* Appbar 的加密切换按钮 */} 116 | {showEncryptionToggle && ( 117 | 121 | 130 | {encryptionEnabled ? : } 131 | 132 | 133 | )} 134 | 135 | 136 | 137 | 138 | {/* 主内容区域 */} 139 | 148 | {children} 149 | 150 | 151 | 152 | {/* 底部导航 - 独立放置在外部 */} 153 | 162 | { 165 | onTabChange(getTabValue(newValue)); 166 | }} 167 | showLabels 168 | sx={{ width: '100%' }} 169 | > 170 | } 175 | /> 176 | } 181 | /> 182 | } 187 | /> 188 | 189 | 190 | 191 | ); 192 | } -------------------------------------------------------------------------------- /entrypoints/popup/components/DeviceSelectV2.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | FormControl, 4 | InputLabel, 5 | Select, 6 | MenuItem, 7 | Box, 8 | Typography, 9 | SelectChangeEvent, 10 | Stack, 11 | Divider, 12 | OutlinedInput, 13 | Checkbox, 14 | ListItemText, 15 | FormHelperText 16 | } from '@mui/material'; 17 | import AddIcon from '@mui/icons-material/Add'; 18 | import { useTranslation } from 'react-i18next'; 19 | import { Device } from '../types'; 20 | 21 | const ITEM_HEIGHT = 48; 22 | const ITEM_PADDING_TOP = 8; 23 | const MenuProps = { 24 | PaperProps: { 25 | style: { 26 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP + 16, 27 | width: 'calc(100% - 120px)', 28 | }, 29 | sx: { 30 | display: 'flex', 31 | flexDirection: 'column', 32 | '& .MuiList-root': { 33 | flex: '1 1 auto', 34 | overflow: 'auto' 35 | } 36 | } 37 | } 38 | }; 39 | 40 | interface DeviceSelectV2Props { 41 | devices: Device[]; 42 | selectedDevices: Device[]; 43 | onDevicesChange: (devices: Device[]) => void; 44 | onAddClick: () => void; 45 | label?: string; 46 | placeholder?: string; 47 | showLabel?: boolean; 48 | defaultDevice?: Device | null; 49 | } 50 | 51 | export default function DeviceSelectV2({ 52 | devices, 53 | selectedDevices, 54 | onDevicesChange, 55 | onAddClick, 56 | label = '选择设备', 57 | placeholder = '请选择设备', 58 | showLabel = true, 59 | defaultDevice = null 60 | }: DeviceSelectV2Props) { 61 | const { t } = useTranslation(); 62 | const [open, setOpen] = useState(false); 63 | 64 | const handleChange = (event: SelectChangeEvent) => { 65 | const selectedIds = typeof event.target.value === 'string' 66 | ? event.target.value.split(',') 67 | : event.target.value; 68 | 69 | const selectedDevicesList = devices.filter(device => selectedIds.includes(device.id)); 70 | onDevicesChange(selectedDevicesList); 71 | }; 72 | 73 | const handleAddClick = (e: React.MouseEvent) => { 74 | e.stopPropagation(); 75 | setOpen(false); 76 | onAddClick(); 77 | }; 78 | 79 | // 获取选中设备的ID列表 80 | const selectedDeviceIds = selectedDevices.map(device => device.id); 81 | 82 | // 渲染选中设备的显示文本 83 | const renderValue = (selected: string[]) => { 84 | if (selected.length === 0) { 85 | return {t('push.select_device')}; 86 | } 87 | const selectedNames = devices 88 | .filter(device => selected.includes(device.id)) 89 | .map(device => device.alias); 90 | return selectedNames.join(', '); 91 | }; 92 | 93 | return ( 94 | 95 | {showLabel && 104 | {/* 目标设备 */} 105 | {t('push.target_device')} 106 | } 107 | 188 | 197 | {selectedDevices.length > 1 ? `${t('push.select_device_length', { count: selectedDevices.length })}` : t('push.select_device_helper')} 198 | 199 | 200 | 201 | ); 202 | } 203 | -------------------------------------------------------------------------------- /public/lottie/coming-soon.json: -------------------------------------------------------------------------------- 1 | {"v":"5.12.2","fr":60,"ip":0,"op":75,"w":48,"h":48,"nm":"history","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"history-outline-top_s1g1_s2g2_s3g1_s4g1_background Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33.266,24,0],"ix":2,"l":2},"a":{"a":0,"k":[19.375,12.736,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,21.486],[6.25,21.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0],"to":[0.5,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[3,0],"to":[0,0],"ti":[0.5,0]},{"t":35,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,12.736],[6.25,12.736]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,12.736],[5.156,12.736]],"c":false}]},{"t":35,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,12.736],[6.25,12.736]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0],"to":[0.5,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[3,0],"to":[0,0],"ti":[0.5,0]},{"t":35,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,3.986],[6.25,3.986]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0],"to":[0.5,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[3,0],"to":[0,0],"ti":[0.5,0]},{"t":35,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[19.533,8.963],[19.533,12.736]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19.531,12.73],"to":[-0.604,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[15.906,12.73],"to":[0,0],"ti":[-0.604,0]},{"t":35,"s":[19.531,12.73]}],"ix":2},"a":{"a":0,"k":[19.531,12.73],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[720]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[19.514,12.736],[22.477,12.736]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19.516,12.738],"to":[-0.604,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[15.891,12.738],"to":[0,0],"ti":[-0.604,0]},{"t":35,"s":[19.516,12.738]}],"ix":2},"a":{"a":0,"k":[19.516,12.738],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":35,"s":[360]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,4.272],[-4.272,0],[0,-4.272],[4.272,0]],"o":[[0,-4.272],[4.272,0],[0,4.272],[-4.272,0]],"v":[[-7.736,0],[0,-7.736],[7.736,0],[0,7.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9529411764705882,0.9529411764705882,0.9529411764705882],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19.514,12.736],"to":[-0.604,0],"ti":[0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[15.889,12.736],"to":[0,0],"ti":[-0.604,0]},{"t":35,"s":[19.514,12.736]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":3,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"history-outline-bot_s1g1_s2g1_s3g1_s4g1_background Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[20.141,24,0],"to":[0.458,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[22.891,24,0],"to":[0,0,0],"ti":[0.458,0,0]},{"t":35,"s":[20.141,24,0]}],"ix":2,"l":2},"a":{"a":0,"k":[18.283,21.636,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.433,0],[0,0],[0,4.433],[0,0],[-4.433,0],[0,0],[0,-4.433],[0,0]],"o":[[0,0],[-4.433,0],[0,0],[0,-4.433],[0,0],[4.433,0],[0,0],[0,4.433]],"v":[[5.256,16.637],[-5.256,16.637],[-13.283,8.609],[-13.283,-8.609],[-5.256,-16.637],[5.256,-16.637],[13.283,-8.609],[13.283,8.609]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.09803921568627451,0.4627450980392157,0.8235294117647058],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9529411764705882,0.9529411764705882,0.9529411764705882],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.283,21.636],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":75,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} -------------------------------------------------------------------------------- /public/_locales/zh_HK/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext_desc": { 3 | "message": "Bark Sender 係一款桌面瀏覽器擴充,可快速將網頁選取文字或剪貼簿內容推送到已安裝 Bark App 嘅 iOS 裝置,仲支援加密傳輸同自訂參數。", 4 | "description": "Extension description" 5 | }, 6 | "extension_installed": { 7 | "message": "Bark Sender 已安裝", 8 | "description": "Extension installed message" 9 | }, 10 | "shortcut_triggered": { 11 | "message": "已接收到全域快速鍵觸發", 12 | "description": "Global shortcut triggered message" 13 | }, 14 | "device_not_found_window": { 15 | "message": "未能找到預設裝置,但仍會開啟視窗以供用戶設定", 16 | "description": "Default device not found but window opened" 17 | }, 18 | "device_not_found": { 19 | "message": "未能找到預設裝置,請先新增裝置", 20 | "description": "Default device not found message" 21 | }, 22 | "creating_small_window": { 23 | "message": "正在建立小型視窗", 24 | "description": "Creating small window message" 25 | }, 26 | "send_message_to_window_failed": { 27 | "message": "發送通知至視窗失敗:$1", 28 | "description": "Failed to send message to window" 29 | }, 30 | "notification_shortcut_with_device": { 31 | "message": "已開啟推送視窗,請點擊「發送剪貼簿內容」按鈕(預設裝置:$1)", 32 | "description": "Notification for shortcut with default device" 33 | }, 34 | "shortcut_processing_failed": { 35 | "message": "快速鍵觸發失敗,請手動開啟擴充功能", 36 | "description": "Shortcut processing failed" 37 | }, 38 | "background_send_push_failed": { 39 | "message": "背景推送發送失敗", 40 | "description": "Background push failed" 41 | }, 42 | "background_send_encrypted_push_failed": { 43 | "message": "背景發送加密推送失敗", 44 | "description": "Background encrypted push failed" 45 | }, 46 | "background_send_encrypted_request_to": { 47 | "message": "背景發出加密請求至:$1", 48 | "description": "Background send encrypted request" 49 | }, 50 | "background_encrypted_request_success": { 51 | "message": "背景加密請求成功", 52 | "description": "Background encrypted request success" 53 | }, 54 | "update_context_menus_failed": { 55 | "message": "更新右鍵功能表失敗", 56 | "description": "Context menu update failed" 57 | }, 58 | "send_selection_to_device": { 59 | "message": "發送選取文字至 $1", 60 | "description": "Send selection to device" 61 | }, 62 | "send_page_to_device": { 63 | "message": "發送目前頁面網址至 $1", 64 | "description": "Send page URL to device" 65 | }, 66 | "send_link_to_device": { 67 | "message": "發送目標連結至 $1", 68 | "description": "Send link URL to device" 69 | }, 70 | "inspect_send": { 71 | "message": "分析並發送至 $1", 72 | "description": "Inspect and send" 73 | }, 74 | "bark_sender_title": { 75 | "message": "Bark Sender", 76 | "description": "Extension title" 77 | }, 78 | "sent_to_device": { 79 | "message": "已發送至 $1", 80 | "description": "Sent to device" 81 | }, 82 | "send_failed_check_network": { 83 | "message": "發送失敗,請檢查網絡連線", 84 | "description": "Send failed, check network" 85 | }, 86 | "save_history_record": { 87 | "message": "已儲存歷史紀錄", 88 | "description": "History record saved" 89 | }, 90 | "current_history_total": { 91 | "message": "目前歷史紀錄總數:$1", 92 | "description": "Current history count" 93 | }, 94 | "save_history_failed": { 95 | "message": "儲存歷史紀錄失敗", 96 | "description": "Save history failed" 97 | }, 98 | "read_clipboard_failed": { 99 | "message": "讀取剪貼簿失敗", 100 | "description": "Read clipboard failed" 101 | }, 102 | "error_unknown": { 103 | "message": "未知錯誤", 104 | "description": "Unknown error" 105 | }, 106 | "enable_speed_mode": { 107 | "message": "啟用極速模式", 108 | "description": "Enable speed mode" 109 | }, 110 | "disable_speed_mode": { 111 | "message": "關閉極速模式", 112 | "description": "Disable speed mode" 113 | }, 114 | "shortcut_send_clipboard_description": { 115 | "message": "發送剪貼簿內容至預設裝置", 116 | "description": "Send clipboard to default device" 117 | }, 118 | "background_image": { 119 | "message": "背景圖片", 120 | "description": "Background image" 121 | }, 122 | "image_content": { 123 | "message": "圖片內容", 124 | "description": "Image content" 125 | }, 126 | "image_link": { 127 | "message": "圖片連結", 128 | "description": "Image link" 129 | }, 130 | "send_image_link": { 131 | "message": "發送圖片連結", 132 | "description": "Send image link" 133 | }, 134 | "image_link_invalid": { 135 | "message": "僅支援發送 http 或 https 協定的圖片連結", 136 | "description": "Invalid image link" 137 | }, 138 | "alt_text": { 139 | "message": "替代文字", 140 | "description": "Alt text" 141 | }, 142 | "send_alt_text": { 143 | "message": "發送替代文字", 144 | "description": "Send alt text" 145 | }, 146 | "image": { 147 | "message": "圖片", 148 | "description": "Image" 149 | }, 150 | "image_source": { 151 | "message": "圖片來源", 152 | "description": "Image source" 153 | }, 154 | "reload_preview": { 155 | "message": "重新載入圖片", 156 | "description": "Reload preview" 157 | }, 158 | "text_content": { 159 | "message": "文字內容", 160 | "description": "Text content" 161 | }, 162 | "tag": { 163 | "message": "標籤", 164 | "description": "Tag" 165 | }, 166 | "no_parent_element": { 167 | "message": "無父元素", 168 | "description": "No parent element" 169 | }, 170 | "current_element": { 171 | "message": "目前元素", 172 | "description": "Current element" 173 | }, 174 | "parent_element": { 175 | "message": "父元素", 176 | "description": "Parent element" 177 | }, 178 | "current_element_text": { 179 | "message": "目前元素文字", 180 | "description": "Current element text" 181 | }, 182 | "send_current_element_text": { 183 | "message": "發送目前元素文字", 184 | "description": "Send current element text" 185 | }, 186 | "parent_element_text": { 187 | "message": "父元素文字", 188 | "description": "Parent element text" 189 | }, 190 | "send_parent_element_text": { 191 | "message": "發送父元素文字", 192 | "description": "Send parent element text" 193 | }, 194 | "page_link": { 195 | "message": "頁面連結", 196 | "description": "Page link" 197 | }, 198 | "page_title": { 199 | "message": "頁面標題", 200 | "description": "Page title" 201 | }, 202 | "send_page_link": { 203 | "message": "發送頁面連結", 204 | "description": "Send page link" 205 | }, 206 | "target_info": { 207 | "message": "目標資訊", 208 | "description": "Target info" 209 | }, 210 | "link_address": { 211 | "message": "連結地址", 212 | "description": "Link address" 213 | }, 214 | "send_link": { 215 | "message": "發送連結", 216 | "description": "Send link" 217 | }, 218 | "selected_text": { 219 | "message": "選取文字", 220 | "description": "Selected text" 221 | }, 222 | "send_selected_text": { 223 | "message": "發送選取文字", 224 | "description": "Send selected text" 225 | }, 226 | "select_content_to_send": { 227 | "message": "請選擇要發送的內容", 228 | "description": "Select content to send" 229 | }, 230 | "close": { 231 | "message": "關閉", 232 | "description": "Close button" 233 | }, 234 | "parse_html_error": { 235 | "message": "解析 HTML 時出現錯誤", 236 | "description": "Parse HTML error" 237 | }, 238 | "omnibox_send_push": { 239 | "message": "推送内容给默认设备", 240 | "description": "Send content to default device" 241 | }, 242 | "large_content_not_last_chunk": { 243 | "message": "長按本消息或向下複製, 這不是最後一段", 244 | "description": "Large content not last chunk" 245 | }, 246 | "large_content_last_chunk": { 247 | "message": "長按或向下複製, 打開通知中心查看其他內容", 248 | "description": "Large content last chunk" 249 | }, 250 | "text_length_tip": { 251 | "message": "文字如果過長,可能會分段推送", 252 | "description": "Text length tip" 253 | } 254 | } -------------------------------------------------------------------------------- /public/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext_desc": { 3 | "message": "Bark Sender 是一款桌面瀏覽器擴充,可快速將網頁選取文字或剪貼簿內容推播到已安裝 Bark App 的 iOS 裝置,並支援加密傳輸及自訂參數。", 4 | "description": "Extension description" 5 | }, 6 | "extension_installed": { 7 | "message": "Bark Sender 已安裝", 8 | "description": "Extension installed message" 9 | }, 10 | "shortcut_triggered": { 11 | "message": "收到全域快速鍵觸發", 12 | "description": "Global shortcut triggered message" 13 | }, 14 | "device_not_found_window": { 15 | "message": "找不到預設裝置,但仍會開啟視窗讓使用者設定", 16 | "description": "Default device not found but window opened" 17 | }, 18 | "device_not_found": { 19 | "message": "找不到預設裝置,請先新增裝置", 20 | "description": "Default device not found message" 21 | }, 22 | "creating_small_window": { 23 | "message": "正在建立小視窗", 24 | "description": "Creating small window message" 25 | }, 26 | "send_message_to_window_failed": { 27 | "message": "發送通知到視窗失敗:$1", 28 | "description": "Failed to send message to window" 29 | }, 30 | "notification_shortcut_with_device": { 31 | "message": "已開啟推播視窗,請點選「發送剪貼簿內容」按鈕(預設裝置:$1)", 32 | "description": "Notification for shortcut with default device" 33 | }, 34 | "shortcut_processing_failed": { 35 | "message": "快速鍵觸發失敗,請手動啟用擴充功能", 36 | "description": "Shortcut processing failed" 37 | }, 38 | "background_send_push_failed": { 39 | "message": "背景推播失敗", 40 | "description": "Background push failed" 41 | }, 42 | "background_send_encrypted_push_failed": { 43 | "message": "背景發送加密推播失敗", 44 | "description": "Background encrypted push failed" 45 | }, 46 | "background_send_encrypted_request_to": { 47 | "message": "背景發送加密請求至:$1", 48 | "description": "Background send encrypted request" 49 | }, 50 | "background_encrypted_request_success": { 51 | "message": "背景加密請求成功", 52 | "description": "Background encrypted request success" 53 | }, 54 | "update_context_menus_failed": { 55 | "message": "更新右鍵選單失敗", 56 | "description": "Context menu update failed" 57 | }, 58 | "send_selection_to_device": { 59 | "message": "發送選取的文字到 $1", 60 | "description": "Send selection to device" 61 | }, 62 | "send_page_to_device": { 63 | "message": "發送目前頁面的網址到 $1", 64 | "description": "Send page URL to device" 65 | }, 66 | "send_link_to_device": { 67 | "message": "發送目標網址到 $1", 68 | "description": "Send link URL to device" 69 | }, 70 | "inspect_send": { 71 | "message": "解析並發送到 $1", 72 | "description": "Inspect and send" 73 | }, 74 | "bark_sender_title": { 75 | "message": "Bark Sender", 76 | "description": "Extension title" 77 | }, 78 | "sent_to_device": { 79 | "message": "已發送至 $1", 80 | "description": "Sent to device" 81 | }, 82 | "send_failed_check_network": { 83 | "message": "發送失敗,請檢查網路連線", 84 | "description": "Send failed, check network" 85 | }, 86 | "save_history_record": { 87 | "message": "歷史紀錄已儲存", 88 | "description": "History record saved" 89 | }, 90 | "current_history_total": { 91 | "message": "目前歷史紀錄總數:$1", 92 | "description": "Current history count" 93 | }, 94 | "save_history_failed": { 95 | "message": "儲存歷史紀錄失敗", 96 | "description": "Save history failed" 97 | }, 98 | "read_clipboard_failed": { 99 | "message": "讀取剪貼簿失敗", 100 | "description": "Read clipboard failed" 101 | }, 102 | "error_unknown": { 103 | "message": "未知錯誤", 104 | "description": "Unknown error" 105 | }, 106 | "enable_speed_mode": { 107 | "message": "啟用極速模式", 108 | "description": "Enable speed mode" 109 | }, 110 | "disable_speed_mode": { 111 | "message": "關閉極速模式", 112 | "description": "Disable speed mode" 113 | }, 114 | "shortcut_send_clipboard_description": { 115 | "message": "將剪貼簿內容發送至預設裝置", 116 | "description": "Send clipboard to default device" 117 | }, 118 | "background_image": { 119 | "message": "背景圖片", 120 | "description": "Background image" 121 | }, 122 | "image_content": { 123 | "message": "圖片內容", 124 | "description": "Image content" 125 | }, 126 | "image_link": { 127 | "message": "圖片連結", 128 | "description": "Image link" 129 | }, 130 | "send_image_link": { 131 | "message": "發送圖片連結", 132 | "description": "Send image link" 133 | }, 134 | "image_link_invalid": { 135 | "message": "僅支援發送 http 或 https 協定的圖片連結", 136 | "description": "Invalid image link" 137 | }, 138 | "alt_text": { 139 | "message": "替代文字", 140 | "description": "Alt text" 141 | }, 142 | "send_alt_text": { 143 | "message": "發送替代文字", 144 | "description": "Send alt text" 145 | }, 146 | "image": { 147 | "message": "圖片", 148 | "description": "Image" 149 | }, 150 | "image_source": { 151 | "message": "圖片來源", 152 | "description": "Image source" 153 | }, 154 | "reload_preview": { 155 | "message": "重新載入圖片", 156 | "description": "Reload preview" 157 | }, 158 | "text_content": { 159 | "message": "文字內容", 160 | "description": "Text content" 161 | }, 162 | "tag": { 163 | "message": "標籤", 164 | "description": "Tag" 165 | }, 166 | "no_parent_element": { 167 | "message": "無父元素", 168 | "description": "No parent element" 169 | }, 170 | "current_element": { 171 | "message": "目前元素", 172 | "description": "Current element" 173 | }, 174 | "parent_element": { 175 | "message": "父元素", 176 | "description": "Parent element" 177 | }, 178 | "current_element_text": { 179 | "message": "目前元素文本", 180 | "description": "Current element text" 181 | }, 182 | "send_current_element_text": { 183 | "message": "發送目前元素文本", 184 | "description": "Send current element text" 185 | }, 186 | "parent_element_text": { 187 | "message": "父元素文字", 188 | "description": "Parent element text" 189 | }, 190 | "send_parent_element_text": { 191 | "message": "發送父元素文字", 192 | "description": "Send parent element text" 193 | }, 194 | "page_link": { 195 | "message": "頁面連結", 196 | "description": "Page link" 197 | }, 198 | "page_title": { 199 | "message": "頁面標題", 200 | "description": "Page title" 201 | }, 202 | "send_page_link": { 203 | "message": "發送頁面連結", 204 | "description": "Send page link" 205 | }, 206 | "target_info": { 207 | "message": "目標資訊", 208 | "description": "Target info" 209 | }, 210 | "link_address": { 211 | "message": "連結網址", 212 | "description": "Link address" 213 | }, 214 | "send_link": { 215 | "message": "發送連結", 216 | "description": "Send link" 217 | }, 218 | "selected_text": { 219 | "message": "選取的文字", 220 | "description": "Selected text" 221 | }, 222 | "send_selected_text": { 223 | "message": "發送選取的文字", 224 | "description": "Send selected text" 225 | }, 226 | "select_content_to_send": { 227 | "message": "請選擇要發送的內容", 228 | "description": "Select content to send" 229 | }, 230 | "close": { 231 | "message": "關閉", 232 | "description": "Close button" 233 | }, 234 | "parse_html_error": { 235 | "message": "解析 HTML 時出現錯誤", 236 | "description": "Parse HTML error" 237 | }, 238 | "omnibox_send_push": { 239 | "message": "發送內容給預設裝置", 240 | "description": "Send content to default device" 241 | }, 242 | "large_content_not_last_chunk": { 243 | "message": "長按本通知或向下複製, 這不是最後一段", 244 | "description": "Large content not last chunk" 245 | }, 246 | "large_content_last_chunk": { 247 | "message": "長按或向下複製, 開啟通知中心查看其他內容", 248 | "description": "Large content last chunk" 249 | }, 250 | "text_length_tip": { 251 | "message": "如果文字過長,可能會分段發送", 252 | "description": "Text length tip" 253 | } 254 | } -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext_desc": { 3 | "message": "Bark Sender 是款桌面浏览器扩展,可快速将网页选中文字或剪贴板内容发送到已安装 Bark App 的 iOS 设备,并支持加密传输及自定义推送参数。", 4 | "description": "Extension description" 5 | }, 6 | "extension_installed": { 7 | "message": "Bark Sender 已安装", 8 | "description": "Extension installed message" 9 | }, 10 | "shortcut_triggered": { 11 | "message": "收到全局快捷键触发", 12 | "description": "Global shortcut triggered message" 13 | }, 14 | "device_not_found_window": { 15 | "message": "未找到默认设备,但仍打开窗口让用户配置", 16 | "description": "Default device not found but window opened" 17 | }, 18 | "device_not_found": { 19 | "message": "未找到默认设备,请先添加设备", 20 | "description": "Default device not found message" 21 | }, 22 | "creating_small_window": { 23 | "message": "创建小窗口", 24 | "description": "Creating small window message" 25 | }, 26 | "send_message_to_window_failed": { 27 | "message": "发送消息到窗口失败:$1", 28 | "description": "Failed to send message to window" 29 | }, 30 | "notification_shortcut_with_device": { 31 | "message": "已打开推送窗口,点击\"发送剪切板内容\"按钮 (默认设备:$1)", 32 | "description": "Notification for shortcut with default device" 33 | }, 34 | "shortcut_processing_failed": { 35 | "message": "快捷键触发失败,请手动打开扩展", 36 | "description": "Shortcut processing failed" 37 | }, 38 | "background_send_push_failed": { 39 | "message": "Background发送推送失败", 40 | "description": "Background push failed" 41 | }, 42 | "background_send_encrypted_push_failed": { 43 | "message": "Background 发送加密推送失败", 44 | "description": "Background encrypted push failed" 45 | }, 46 | "background_send_encrypted_request_to": { 47 | "message": "Background 发送加密请求到:$1", 48 | "description": "Background send encrypted request" 49 | }, 50 | "background_encrypted_request_success": { 51 | "message": "Background 加密请求成功", 52 | "description": "Background encrypted request success" 53 | }, 54 | "update_context_menus_failed": { 55 | "message": "更新右键菜单失败", 56 | "description": "Context menu update failed" 57 | }, 58 | "send_selection_to_device": { 59 | "message": "发送选中的文本给 $1", 60 | "description": "Send selection to device" 61 | }, 62 | "send_page_to_device": { 63 | "message": "发送当前页面的网址给 $1", 64 | "description": "Send page URL to device" 65 | }, 66 | "send_link_to_device": { 67 | "message": "发送目标网址给 $1", 68 | "description": "Send link URL to device" 69 | }, 70 | "inspect_send": { 71 | "message": "解析并发送给 $1", 72 | "description": "Inspect and send" 73 | }, 74 | "bark_sender_title": { 75 | "message": "Bark Sender", 76 | "description": "Extension title" 77 | }, 78 | "sent_to_device": { 79 | "message": "已发送到 $1", 80 | "description": "Sent to device" 81 | }, 82 | "send_failed_check_network": { 83 | "message": "发送失败,请检查网络连接", 84 | "description": "Send failed, check network" 85 | }, 86 | "save_history_record": { 87 | "message": "历史记录已保存", 88 | "description": "History record saved" 89 | }, 90 | "current_history_total": { 91 | "message": "当前历史记录总数:$1", 92 | "description": "Current history count" 93 | }, 94 | "save_history_failed": { 95 | "message": "保存历史记录失败", 96 | "description": "Save history failed" 97 | }, 98 | "read_clipboard_failed": { 99 | "message": "读取剪切板失败", 100 | "description": "Read clipboard failed" 101 | }, 102 | "error_unknown": { 103 | "message": "未知错误", 104 | "description": "Unknown error" 105 | }, 106 | "enable_speed_mode": { 107 | "message": "开启极速模式", 108 | "description": "Enable speed mode" 109 | }, 110 | "disable_speed_mode": { 111 | "message": "关闭极速模式", 112 | "description": "Disable speed mode" 113 | }, 114 | "shortcut_send_clipboard_description": { 115 | "message": "发送剪切板内容到默认设备", 116 | "description": "Send clipboard to default device" 117 | }, 118 | "background_image": { 119 | "message": "背景图片", 120 | "description": "Background image" 121 | }, 122 | "image_content": { 123 | "message": "图片内容", 124 | "description": "Image content" 125 | }, 126 | "image_link": { 127 | "message": "图片链接", 128 | "description": "Image link" 129 | }, 130 | "send_image_link": { 131 | "message": "发送图片链接", 132 | "description": "Send image link" 133 | }, 134 | "image_link_invalid": { 135 | "message": "只能发送 http 或 https 协议的图片链接", 136 | "description": "Invalid image link" 137 | }, 138 | "alt_text": { 139 | "message": "替代文本", 140 | "description": "Alt text" 141 | }, 142 | "send_alt_text": { 143 | "message": "发送替代文本", 144 | "description": "Send alt text" 145 | }, 146 | "image": { 147 | "message": "图片", 148 | "description": "Image" 149 | }, 150 | "image_source": { 151 | "message": "图片来源", 152 | "description": "Image source" 153 | }, 154 | "reload_preview": { 155 | "message": "重新加载图片", 156 | "description": "Reload preview" 157 | }, 158 | "text_content": { 159 | "message": "文字内容", 160 | "description": "Text content" 161 | }, 162 | "tag": { 163 | "message": "标签", 164 | "description": "Tag" 165 | }, 166 | "no_parent_element": { 167 | "message": "无父元素", 168 | "description": "No parent element" 169 | }, 170 | "current_element": { 171 | "message": "当前元素", 172 | "description": "Current element" 173 | }, 174 | "parent_element": { 175 | "message": "父元素", 176 | "description": "Parent element" 177 | }, 178 | "current_element_text": { 179 | "message": "当前元素文本", 180 | "description": "Current element text" 181 | }, 182 | "send_current_element_text": { 183 | "message": "发送当前元素文本", 184 | "description": "Send current element text" 185 | }, 186 | "parent_element_text": { 187 | "message": "父元素文本", 188 | "description": "Parent element text" 189 | }, 190 | "send_parent_element_text": { 191 | "message": "发送父元素文本", 192 | "description": "Send parent element text" 193 | }, 194 | "page_link": { 195 | "message": "页面链接", 196 | "description": "Page link" 197 | }, 198 | "page_title": { 199 | "message": "页面标题", 200 | "description": "Page title" 201 | }, 202 | "send_page_link": { 203 | "message": "发送页面链接", 204 | "description": "Send page link" 205 | }, 206 | "target_info": { 207 | "message": "目标信息", 208 | "description": "Target info" 209 | }, 210 | "link_address": { 211 | "message": "链接地址", 212 | "description": "Link address" 213 | }, 214 | "send_link": { 215 | "message": "发送链接", 216 | "description": "Send link" 217 | }, 218 | "selected_text": { 219 | "message": "选中文本", 220 | "description": "Selected text" 221 | }, 222 | "send_selected_text": { 223 | "message": "发送选中文本", 224 | "description": "Send selected text" 225 | }, 226 | "select_content_to_send": { 227 | "message": "选择要发送的内容", 228 | "description": "Select content to send" 229 | }, 230 | "close": { 231 | "message": "关闭", 232 | "description": "Close button" 233 | }, 234 | "parse_html_error": { 235 | "message": "解析 HTML 时出错", 236 | "description": "Parse HTML error" 237 | }, 238 | "omnibox_send_push": { 239 | "message": "推送内容给默认设备", 240 | "description": "Send content to default device" 241 | }, 242 | "large_content_not_last_chunk": { 243 | "message": "长按本消息或向下滚动复制, 这不是最后一段", 244 | "description": "Large content not last chunk" 245 | }, 246 | "large_content_last_chunk": { 247 | "message": "长按或向下滚动复制, 打开通知中心查看其他内容", 248 | "description": "Large content last chunk" 249 | }, 250 | "text_length_tip": { 251 | "message": "文本如果过长,可能会分段发送", 252 | "description": "Text length tip" 253 | } 254 | } -------------------------------------------------------------------------------- /wxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'wxt'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | // See https://wxt.dev/api/config.html 6 | export default defineConfig({ 7 | modules: ['@wxt-dev/module-react'], 8 | manifestVersion: 3, 9 | vite: () => ({ 10 | build: { 11 | minify: 'terser', 12 | rollupOptions: { 13 | output: { 14 | minifyInternalExports: true 15 | } 16 | } 17 | } 18 | }), 19 | manifest: { 20 | default_locale: 'en', 21 | omnibox: { 22 | keyword: 'bb' 23 | }, 24 | permissions: [ 25 | 'storage', 26 | 'contextMenus', 27 | 'activeTab', 28 | 'notifications', 29 | 'clipboardRead', // Firefox 不支持 clipboardRead 权限 30 | 'identity' // Chrome 特有 31 | ], 32 | host_permissions: [ 33 | 'https://*/*', 34 | 'http://*/*' 35 | ], 36 | content_security_policy: { 37 | extension_pages: "script-src 'self'; object-src 'self';" 38 | }, 39 | oauth2: { 40 | client_id: "1015101043935-97vmnmdsqgql2lne6stun1b4lhluhtgc.apps.googleusercontent.com", 41 | scopes: ["https://www.googleapis.com/auth/drive.file"] 42 | }, 43 | commands: { 44 | "send-clipboard": { 45 | "suggested_key": { 46 | "default": "Ctrl+Shift+8", 47 | "mac": "Command+Shift+8" 48 | }, 49 | "description": "__MSG_shortcut_send_clipboard_description__", 50 | "global": true 51 | } 52 | } 53 | }, 54 | hooks: { 55 | 'build:done': (wxt, output) => { 56 | // console.log('构建完成, Node 正在处理 content-scripts 中的模版字符串里代码缩进导致的空格'); 57 | const outputDir = path.resolve(process.cwd(), '.output'); 58 | 59 | fs.readdirSync(outputDir).forEach(browser => { 60 | const browserDir = path.join(outputDir, browser); 61 | 62 | if (fs.statSync(browserDir).isDirectory()) { 63 | const contentScriptsDir = path.join(browserDir, 'content-scripts'); 64 | 65 | if (fs.existsSync(contentScriptsDir)) { 66 | // 处理 content-scripts 目录中的所有 js 67 | fs.readdirSync(contentScriptsDir).forEach(file => { 68 | if (file.endsWith('.js')) { 69 | const filePath = path.join(contentScriptsDir, file); 70 | 71 | let content = fs.readFileSync(filePath, 'utf8'); 72 | const originalContent = content; 73 | 74 | // 处理 \n 后面跟着空格的情况 75 | content = content.replace(/\\n\s+/g, ''); 76 | 77 | // 匹配 css 注释 /* css start */ ... /* css end */ 中间的 css 内容, 并压缩 CSS 78 | content = content.replace(/\/\*\s*css\s+start\s*\*\/([\s\S]*?)\/\*\s*css\s+end\s*\*\//g, (match, cssContent) => { 79 | // 移除 CSS 内容中的所有 \n 并压缩 CSS 80 | let processedMatch = match 81 | .replace(/\\n/g, '') // 移除所有换行符 82 | .replace(/(\s)*{\s*/g, "{") // 压缩花括号前后的空格 83 | .replace(/(\s)*}\s*/g, "}") // 压缩花括号后的空格 84 | .replace(/(\s)*;\s*/g, ";") // 压缩分号前后的空格 85 | .replace(/:(\s)*/g, ":") // 压缩冒号后的空格 86 | .replace(/;}/g, "}"); // 移除花括号前的分号 87 | // 这里实现参考: https://www.zhangxinxu.com/sp/css-compress-mini.html 88 | return processedMatch; 89 | }); 90 | 91 | // 匹配 /* ... */ css 注释, 移除注释内容 92 | content = content.replace(/\/\*\s+.+?\s\*\//g, ''); 93 | 94 | if (content !== originalContent) { 95 | fs.writeFileSync(filePath, content, 'utf8'); 96 | // console.log(`已处理文件: ${filePath}`); 97 | } 98 | } 99 | }); 100 | } 101 | 102 | // hack: 移除不安全的 new Function 调用 (针对 ag-grid@34.2.0) 103 | const chunksDir = path.join(browserDir, 'chunks'); 104 | if (fs.existsSync(chunksDir)) { 105 | fs.readdirSync(chunksDir).forEach(file => { 106 | if (file.endsWith('.js')) { 107 | const filePath = path.join(chunksDir, file); 108 | let content = fs.readFileSync(filePath, 'utf8'); 109 | const originalContent = content; 110 | 111 | content = content.replace( 112 | // /new Function\s*\([^)]*\)/g, 113 | /new Function\s*\([^)]*\)/, 114 | 'function() { return null; }' // ag-grid@34.2.0 的 new Function 调用只产生了一处 115 | ); 116 | if (content !== originalContent) { 117 | fs.writeFileSync(filePath, content, 'utf8'); 118 | console.log(`移除不安全的 new Function 调用: ${filePath}`); 119 | } 120 | } 121 | }); 122 | } 123 | } 124 | }); 125 | }, 126 | 'build:manifestGenerated': (wxt, manifest) => { 127 | if (wxt.config.mode === 'development' && import.meta.env.VITE_DEV_KEY) { 128 | manifest.key = import.meta.env.VITE_DEV_KEY 129 | // https://developer.chrome.com/docs/extensions/how-to/integrate/oauth#upload_to_dashboard 130 | } 131 | /* 132 | 如果是 Firefox: 133 | 1. 移除 global 属性, Firefox 不支持 global 属性 134 | source: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/commands#browser_compatibility 135 | 2. clipboardRead 也不支持, 实际测试会被忽略所以没必要移除 136 | 3. 需要增加 "browser_specific_settings": { 137 | "gecko": { 138 | "id": "bark-sender@uuphy.com", 139 | "strict_min_version": "109.0" 140 | } 141 | }; 142 | source: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings 143 | 如果是Edge: 144 | 移除 identity 和 oauth2,保留其他功能 145 | */ 146 | if (wxt.config.browser === 'firefox') { 147 | if (manifest.commands && manifest.commands['send-clipboard']) { 148 | delete manifest.commands['send-clipboard'].global; 149 | } 150 | 151 | if (manifest.permissions) { 152 | manifest.permissions = manifest.permissions.filter( 153 | (permission: string) => permission !== 'identity' 154 | ); 155 | } 156 | delete manifest.oauth2; 157 | 158 | manifest.browser_specific_settings = { 159 | gecko: { 160 | id: 'bark_sender@uuphy.com', 161 | strict_min_version: '109.0', 162 | data_collection_permissions: { 163 | "required": ["none"] 164 | } 165 | } 166 | }; 167 | } else if (wxt.config.browser === 'edge') { 168 | if (manifest.permissions) { 169 | manifest.permissions = manifest.permissions.filter( 170 | (permission: string) => permission !== 'identity' 171 | ); 172 | } 173 | delete manifest.oauth2; 174 | } else if (wxt.config.browser === 'safari') { 175 | if (manifest.permissions) { 176 | // 移除 Safari 不支持的权限 177 | manifest.permissions = manifest.permissions.filter( 178 | (permission: string) => !['identity', 'notifications'].includes(permission) 179 | ); 180 | } 181 | 182 | delete manifest.oauth2; 183 | delete manifest.omnibox; // Safari 不支持 omnibox 184 | 185 | 186 | if (manifest.commands && manifest.commands['send-clipboard']) { 187 | delete manifest.commands['send-clipboard'].global; 188 | } 189 | 190 | // Safari 需要 content security policy 191 | if (manifest.content_security_policy) { 192 | manifest.content_security_policy = { 193 | extension_pages: "script-src 'self'; object-src 'self';" 194 | }; 195 | } 196 | 197 | if (manifest.background && 'service_worker' in manifest.background) { 198 | const serviceWorkerScript = manifest.background.service_worker; 199 | manifest.background = { 200 | scripts: [serviceWorkerScript], 201 | persistent: false 202 | }; 203 | } 204 | 205 | if (manifest.permissions) { 206 | manifest.permissions.push('nativeMessaging'); 207 | } 208 | 209 | manifest.nativeMessagingHosts = ['com.uuphy.bark-sender']; 210 | } 211 | } 212 | } 213 | }); 214 | --------------------------------------------------------------------------------