├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------