diff --git a/src/assets/svg/close-circle.svg b/src/assets/svg/close-circle.svg new file mode 100644 index 00000000..7d60d052 --- /dev/null +++ b/src/assets/svg/close-circle.svg @@ -0,0 +1 @@ + diff --git a/src/background/actionListener/configManager.ts b/src/background/actionListener/configManager.ts new file mode 100644 index 00000000..e3cea149 --- /dev/null +++ b/src/background/actionListener/configManager.ts @@ -0,0 +1,35 @@ +import { levitateConfigManager } from '@/background/core/configManager/levitate'; +import { + IOperateConfigManagerData, + OperateConfigManagerEnum, +} from '@/isomorphic/background/configManager'; +import { wordMarkConfigManager } from '../core/configManager/wordMark'; +import { RequestMessage } from './index'; + +const managerMap = { + wordMark: wordMarkConfigManager, + levitate: levitateConfigManager, +}; + +export async function createManagerConfigActionListener( + request: RequestMessage, + callback: (params: any) => void, +) { + const { type, value, key, managerType, option = {} } = request.data; + const manage = managerMap[managerType]; + switch (type) { + case OperateConfigManagerEnum.get: { + const result = await manage.get(); + callback(result); + break; + } + case OperateConfigManagerEnum.update: { + const res = await manage.update(key, value, option); + callback(res); + break; + } + default: { + break; + } + } +} diff --git a/src/background/actionListener/index.ts b/src/background/actionListener/index.ts index 5cdd9653..59d66a33 100644 --- a/src/background/actionListener/index.ts +++ b/src/background/actionListener/index.ts @@ -6,7 +6,7 @@ import { createClipActionListener } from './clip'; import { createTabActionListener } from './tab'; import { createSidePanelActionListener } from './sidePanel'; import { createRequestActionListener } from './request'; -import { createWordMarkConfigActionListener } from './wordMarkConfig'; +import { createManagerConfigActionListener } from './configManager'; type MessageSender = chrome.runtime.MessageSender; @@ -49,8 +49,8 @@ export const initBackGroundActionListener = () => { createRequestActionListener(request, sendResponse); break; } - case BackgroundEvents.OperateWordMarkConfig: { - createWordMarkConfigActionListener(request, sendResponse); + case BackgroundEvents.OperateManagerConfig: { + createManagerConfigActionListener(request, sendResponse); break; } default: { diff --git a/src/background/actionListener/wordMarkConfig.ts b/src/background/actionListener/wordMarkConfig.ts deleted file mode 100644 index 35b58b5d..00000000 --- a/src/background/actionListener/wordMarkConfig.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RequestMessage } from './index'; -import { - IOperateWordMarkConfigData, - OperateWordConfigMarkEnum, -} from '@/isomorphic/background/wordMarkConfig'; -import { wordMarkConfigManager } from '@/background/core/wordMarkConfig'; - -export async function createWordMarkConfigActionListener( - request: RequestMessage, - callback: (params: any) => void, -) { - const { type, value, key, option } = request.data; - switch (type) { - case OperateWordConfigMarkEnum.get: { - const result = await wordMarkConfigManager.get(); - callback(result); - break; - } - case OperateWordConfigMarkEnum.update: { - const res = await wordMarkConfigManager.update(key, value, option); - callback(res); - break; - } - default: { - break; - } - } -} diff --git a/src/background/core/configManager/levitate.ts b/src/background/core/configManager/levitate.ts new file mode 100644 index 00000000..9979b76a --- /dev/null +++ b/src/background/core/configManager/levitate.ts @@ -0,0 +1,57 @@ +import Chrome from '@/background/core/chrome'; +import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; +import { + defaultLevitateConfig, + ILevitateConfig, + LevitateConfigKey, +} from '@/isomorphic/constant/levitate'; +import { IConfigManagerOption } from '@/isomorphic/background/configManager'; +import { STORAGE_KEYS } from '@/config'; +import Storage from '../storage'; + +class LevitateConfigManager { + async get() { + const config: ILevitateConfig = + (await Storage.get(STORAGE_KEYS.SETTINGS.LEVITATE_BALL_CONFIG)) || {}; + + // 做一次 config 的合并,保证获取时一定包含 config 中的每一个元素 + for (const _key of Object.keys(defaultLevitateConfig)) { + const key = _key as LevitateConfigKey; + const value = config[key]; + if (typeof value === 'undefined') { + config[key] = defaultLevitateConfig[key] as never; + } + } + + return config; + } + + async update(key: string, value: any, option?: IConfigManagerOption) { + const config = await this.get(); + const result: ILevitateConfig = { + ...config, + [key]: value, + }; + await Chrome.storage.local.set({ + [STORAGE_KEYS.SETTINGS.LEVITATE_BALL_CONFIG]: result, + }); + this.noticeWebPage(result); + return result; + } + + private noticeWebPage(config: ILevitateConfig) { + // 异步通知页面 config 发生了改变 + Chrome.tabs.query({ status: 'complete' }, tabs => { + for (const tab of tabs) { + if (tab.id) { + Chrome.tabs.sendMessage(tab.id, { + action: ContentScriptEvents.LevitateConfigChange, + data: config, + }); + } + } + }); + } +} + +export const levitateConfigManager = new LevitateConfigManager(); diff --git a/src/background/core/wordMarkConfig.ts b/src/background/core/configManager/wordMark.ts similarity index 84% rename from src/background/core/wordMarkConfig.ts rename to src/background/core/configManager/wordMark.ts index 6fb31346..427e2710 100644 --- a/src/background/core/wordMarkConfig.ts +++ b/src/background/core/configManager/wordMark.ts @@ -4,10 +4,10 @@ import { defaultWordMarkConfig, } from '@/isomorphic/constant/wordMark'; import Chrome from '@/background/core/chrome'; -import { IWordMarkConfigOption } from '@/isomorphic/background/wordMarkConfig'; +import { IConfigManagerOption } from '@/isomorphic/background/configManager'; import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; import { STORAGE_KEYS } from '@/config'; -import Storage from './storage'; +import Storage from '../storage'; class WordMarkConfigManager { async get() { @@ -19,18 +19,14 @@ class WordMarkConfigManager { const key = _key as keyof IWordMarkConfig; const value = config[key]; if (typeof value === 'undefined') { - config[key] = defaultWordMarkConfig[key] as any; + config[key] = defaultWordMarkConfig[key] as never; } } return config; } - async update( - key: WordMarkConfigKey, - value: any, - option?: IWordMarkConfigOption, - ) { + async update(key: string, value: any, option?: IConfigManagerOption) { const config = await this.get(); const result: IWordMarkConfig = { ...config, @@ -50,7 +46,7 @@ class WordMarkConfigManager { private noticeWebPage( config: IWordMarkConfig, - option?: IWordMarkConfigOption, + option?: IConfigManagerOption, ) { const { notice = false } = option || {}; if (!notice) { diff --git a/src/components/DisableUrlCard/index.module.less b/src/components/DisableUrlCard/index.module.less new file mode 100644 index 00000000..ef1f9ca5 --- /dev/null +++ b/src/components/DisableUrlCard/index.module.less @@ -0,0 +1,50 @@ +@import '~@/styles/parameters.less'; + +.wrapper { + display: flex; + align-items: center; + border-radius: 8px; + box-shadow: @panel-default-box-shadow; + padding: 12px; + border: 1px solid @border-light-color; + flex-wrap: wrap; + gap: 16px; + + .cardItemWrapper { + display: flex; + gap: 12px; + padding: 6px 12px; + align-items: center; + background-color: @grey-2; + border-radius: 8px; + width: calc(50% - 8px); + border: 1px solid @border-color-primary; + + .icon { + width: 16px; + height: 16px; + border-radius: 50%; + flex: 0 0 auto; + } + + .name { + overflow: hidden; + flex: 1 1 auto; + white-space: nowrap; + text-overflow: ellipsis; + } + + .deleteWrapper { + padding: 4px; + display: flex; + align-items: center; + cursor: pointer; + flex: 0 0 auto; + border-radius: 4px; + + &:hover { + background-color: @bg-primary-hover; + } + } + } +} diff --git a/src/components/DisableUrlCard/index.tsx b/src/components/DisableUrlCard/index.tsx new file mode 100644 index 00000000..efd59158 --- /dev/null +++ b/src/components/DisableUrlCard/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { DeleteOutlined } from '@ant-design/icons'; +import styles from './index.module.less'; + +export interface IDisableUrlItem { + icon: string; + origin: string; +} + +interface IDisableUrlCardProps { + options: Array; + onDelete: (item: IDisableUrlItem, index: number) => void; +} + +function DisableUrlCard(props: IDisableUrlCardProps) { + const { options = [] } = props; + if (!options.length) { + return null; + } + return ( +
+ {options.map((item, index) => { + return ( +
+ + {item.origin} +
props.onDelete(item, index)} + > + +
+
+ ); + })} +
+ ); +} + +export default React.memo(DisableUrlCard); diff --git a/src/components/SelectSavePosition/index.tsx b/src/components/SelectSavePosition/index.tsx index f3661424..4185e061 100644 --- a/src/components/SelectSavePosition/index.tsx +++ b/src/components/SelectSavePosition/index.tsx @@ -52,8 +52,7 @@ function SelectSavePosition(props: ISelectSavePositionProps) { return; } const defaultSavePosition = await backgroundBridge.storage.get(rememberKey); - const positionItem = (defaultSavePosition || - DefaultSavePosition) as ISavePosition; + const positionItem = (defaultSavePosition || DefaultSavePosition) as ISavePosition; initPositionState(positionItem); }; diff --git a/src/components/WordMarkLayout/index.tsx b/src/components/WordMarkLayout/index.tsx index 943aed9a..fb555b51 100644 --- a/src/components/WordMarkLayout/index.tsx +++ b/src/components/WordMarkLayout/index.tsx @@ -28,7 +28,7 @@ function WordMarkLayout(props: IWordMarkLayoutProps) { }; useEffect(() => { - backgroundBridge.wordMarkConfig.get().then(res => { + backgroundBridge.configManager.get('wordMark').then(res => { setWordMarkConfig(res); }); }, []); diff --git a/src/config.ts b/src/config.ts index ae798815..8244d814 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,6 +15,7 @@ export const STORAGE_KEYS = { }, SETTINGS: { WORD_MARK_CONFIG: 'settings/word-mark-config', + LEVITATE_BALL_CONFIG: 'settings/levitate-ball-config', }, NOTE: { SELECT_TAGS: 'note/select-tags', diff --git a/src/core/bridge/background/configManager.ts b/src/core/bridge/background/configManager.ts new file mode 100644 index 00000000..b28f9c84 --- /dev/null +++ b/src/core/bridge/background/configManager.ts @@ -0,0 +1,49 @@ +import { BackgroundEvents } from '@/isomorphic/background'; +import { + OperateConfigManagerEnum, + IConfigManagerOption, + ManagerType, + ManagerKey, +} from '@/isomorphic/background/configManager'; +import type { ICallBridgeImpl } from './index'; + +export function createConfigManagerBridge(impl: ICallBridgeImpl) { + return { + configManager: { + async update( + managerType: ManagerType, + key: ManagerKey, + value: any, + option?: IConfigManagerOption, + ): Promise { + return new Promise(resolve => { + impl( + BackgroundEvents.OperateManagerConfig, + { + type: OperateConfigManagerEnum.update, + key, + value, + option, + managerType, + }, + () => { + resolve(true); + }, + ); + }); + }, + + async get(managerType: ManagerType): Promise { + return new Promise(resolve => { + impl( + BackgroundEvents.OperateManagerConfig, + { type: OperateConfigManagerEnum.get, managerType }, + res => { + resolve(res); + }, + ); + }); + }, + }, + }; +} diff --git a/src/core/bridge/background/index.ts b/src/core/bridge/background/index.ts index 4bfb5dce..b214c410 100644 --- a/src/core/bridge/background/index.ts +++ b/src/core/bridge/background/index.ts @@ -6,7 +6,7 @@ import { createClipBridge } from './clip'; import { createTabBridge } from './tab'; import { createSidePanelBridge } from './sidePanel'; import { createRequestBridge } from './request'; -import { createWordMarkConfigBridge } from './wordMarkConfig'; +import { createConfigManagerBridge } from './configManager'; export interface IBridgeParams { [key: string]: any; @@ -53,7 +53,7 @@ export function createBridges(impl: ICallBridgeImpl) { ...createTabBridge(impl), ...createSidePanelBridge(impl), ...createRequestBridge(impl), - ...createWordMarkConfigBridge(impl), + ...createConfigManagerBridge(impl), }; } diff --git a/src/core/bridge/background/stroge.ts b/src/core/bridge/background/stroge.ts index 7b66a58c..5c97db61 100644 --- a/src/core/bridge/background/stroge.ts +++ b/src/core/bridge/background/stroge.ts @@ -1,12 +1,11 @@ import { BackgroundEvents } from '@/isomorphic/background'; import { OperateStorageEnum } from '@/isomorphic/background/storage'; -import { IUser } from '@/isomorphic/interface'; import { ICallBridgeImpl } from './index'; export function createStorageBridge(impl: ICallBridgeImpl) { return { storage: { - async get(key: string): Promise { + async get(key: string): Promise { return new Promise(resolve => { impl( BackgroundEvents.OperateStorage, diff --git a/src/core/bridge/background/wordMarkConfig.ts b/src/core/bridge/background/wordMarkConfig.ts deleted file mode 100644 index 6a1277af..00000000 --- a/src/core/bridge/background/wordMarkConfig.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BackgroundEvents } from '@/isomorphic/background'; -import { - OperateWordConfigMarkEnum, - IWordMarkConfigOption, -} from '@/isomorphic/background/wordMarkConfig'; -import { IWordMarkConfig, WordMarkConfigKey } from '@/isomorphic/constant/wordMark'; -import type { ICallBridgeImpl } from './index'; - -export function createWordMarkConfigBridge(impl: ICallBridgeImpl) { - return { - wordMarkConfig: { - async update( - key: WordMarkConfigKey, - value: any, - option?: IWordMarkConfigOption, - ): Promise { - return new Promise(resolve => { - impl( - BackgroundEvents.OperateWordMarkConfig, - { type: OperateWordConfigMarkEnum.update, key, value, option }, - () => { - resolve(true); - }, - ); - }); - }, - - async get(): Promise { - return new Promise(resolve => { - impl( - BackgroundEvents.OperateWordMarkConfig, - { type: OperateWordConfigMarkEnum.get }, - (res: IWordMarkConfig) => { - resolve(res); - }, - ); - }); - }, - }, - }; -} diff --git a/src/isomorphic/background/configManager.ts b/src/isomorphic/background/configManager.ts new file mode 100644 index 00000000..56ad9c11 --- /dev/null +++ b/src/isomorphic/background/configManager.ts @@ -0,0 +1,22 @@ +import { LevitateConfigKey } from '../constant/levitate'; +import { WordMarkConfigKey } from '../constant/wordMark'; + +export enum OperateConfigManagerEnum { + get = 'get', + update = 'update', +} + +export type ManagerType = 'wordMark' | 'levitate'; +export type ManagerKey = WordMarkConfigKey | LevitateConfigKey; + +export interface IConfigManagerOption { + notice?: boolean; +} + +export interface IOperateConfigManagerData { + managerType: ManagerType; + type: OperateConfigManagerEnum; + key: ManagerKey; + value?: any; + option?: IConfigManagerOption; +} diff --git a/src/isomorphic/background/index.ts b/src/isomorphic/background/index.ts index c8230f56..d47c4d93 100644 --- a/src/isomorphic/background/index.ts +++ b/src/isomorphic/background/index.ts @@ -6,5 +6,5 @@ export enum BackgroundEvents { OperateTab = 'background/operate-tab', OperateSidePanel = 'background/operate-sidePanel', OperateRequest = 'background/operate-request', - OperateWordMarkConfig = 'background/operate-wordMarkConfig', + OperateManagerConfig = 'background/operate-config-manager', } diff --git a/src/isomorphic/background/wordMarkConfig.ts b/src/isomorphic/background/wordMarkConfig.ts deleted file mode 100644 index 6a3d218c..00000000 --- a/src/isomorphic/background/wordMarkConfig.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { WordMarkConfigKey } from '../constant/wordMark'; - -export enum OperateWordConfigMarkEnum { - get = 'get', - update = 'update', -} - -export interface IWordMarkConfigOption { - notice?: boolean; -} - -export interface IOperateWordMarkConfigData { - type: OperateWordConfigMarkEnum; - key: WordMarkConfigKey; - value?: any; - option?: IWordMarkConfigOption; -} diff --git a/src/isomorphic/constant/levitate.ts b/src/isomorphic/constant/levitate.ts new file mode 100644 index 00000000..b098c2e3 --- /dev/null +++ b/src/isomorphic/constant/levitate.ts @@ -0,0 +1,16 @@ +export type LevitateConfigKey = 'enable' | 'disableUrl' | 'position'; + +export interface ILevitateConfig { + disableUrl: Array<{ + origin: string; + icon: string; + }>; + position: string; + enable: boolean; +} + +export const defaultLevitateConfig: ILevitateConfig = { + enable: true, + disableUrl: [], + position: '', +}; diff --git a/src/isomorphic/event/contentScript.ts b/src/isomorphic/event/contentScript.ts index c62c6e2a..e8348201 100644 --- a/src/isomorphic/event/contentScript.ts +++ b/src/isomorphic/event/contentScript.ts @@ -4,6 +4,7 @@ export enum ContentScriptEvents { CollectLink = 'contentScript/collectLink', ToggleSidePanel = 'contentScript/toggleSidePanel', WordMarkConfigChange = 'contentScript/wordMarkConfigChange', + LevitateConfigChange = 'contentScript/levitateConfigChange', AddContentToClipAssistant = 'contentScript/addContentToClipAssistant', ForceUpgradeVersion = 'contentScript/forceUpgradeVersion', LoginOut = 'contentScript/LoginOut', diff --git a/src/isomorphic/event/levitateBall.ts b/src/isomorphic/event/levitateBall.ts new file mode 100644 index 00000000..4744bb6b --- /dev/null +++ b/src/isomorphic/event/levitateBall.ts @@ -0,0 +1,5 @@ +export const LevitateBallMessageKey = 'LevitateBallMessageKey'; + +export enum LevitateBallMessageActions { + levitateBallConfigUpdate = 'levitateBallConfigUpdate', +} diff --git a/src/isomorphic/link-helper.ts b/src/isomorphic/link-helper.ts index e423430c..2f148522 100644 --- a/src/isomorphic/link-helper.ts +++ b/src/isomorphic/link-helper.ts @@ -20,6 +20,7 @@ const LinkHelper = { settingPage: chrome.runtime.getURL('setting.html'), wordMarkSettingPage: `${chrome.runtime.getURL('setting.html')}?page=wordMark`, shortcutSettingPage: `${chrome.runtime.getURL('setting.html')}?page=shortcut`, + sidePanelSettingPage: `${chrome.runtime.getURL('setting.html')}?page=sidePanel`, }; export default LinkHelper; diff --git a/src/pages/inject/LevitateBall/app.module.less b/src/pages/inject/LevitateBall/app.module.less new file mode 100644 index 00000000..04b0726a --- /dev/null +++ b/src/pages/inject/LevitateBall/app.module.less @@ -0,0 +1,130 @@ +@import '~@/styles/parameters.less'; + +.wrapper { + position: fixed; + right: 0; + top: 50%; + z-index: @z-index-max - 1; + + .ballEntry { + max-width: 34px; + transition: all 0.3s ease-in-out; + display: flex; + position: relative; + + .ballWrapper { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + user-select: none; + border: 1px solid @border-color; + border-radius: 32px 0 0 32px; + box-shadow: 0 3.2px 12px #00000014, 0 5px 25px #0000000a; + background: @white; + opacity: 0.62; + cursor: pointer; + padding: 4px 12px 4px 4px; + gap: 4px; + .logo { + font-size: 24px; + flex: 0 0 auto; + } + + .hiddenWrapper { + display: flex; + white-space: nowrap; + flex: 0 0 auto; + color: @text-primary; + font-size: @font-size; + } + } + } + + .closeWrapper { + height: 20px; + width: 100%; + display: flex; + flex-direction: row-reverse; + + .closeIcon { + transform: translateX(16px); + transition: all 0.3s ease-in-out; + cursor: pointer; + color: @text-color-secondary; + } + } +} + +.wrapper.wrapperHover { + .ballEntry { + max-width: 160px; + + .ballWrapper { + opacity: 1; + } + + .closeWrapper { + transform: translateX(0) translateY(-100%); + } + } + .closeWrapper { + .closeIcon { + transform: translateX(-4px); + } + } +} + +.dragBarMask { + position: fixed; + width: 100vw; + height: 100vh; + left: 0; + top: 0; + bottom: 0; + z-index: @z-index-max + 1; +} + +.disableModal { + z-index: @z-index-max + 2 !important; + + .radioGroup { + display: flex; + flex-direction: column; + } + + .linkWrapper { + color: @text-color-secondary; + margin-top: 4px; + + .link { + color: @link-color; + text-decoration: none; + display: inline-flex; + outline: none; + padding: 0 4px; + cursor: pointer; + } + } + + .disableModalFooter { + display: flex; + justify-content: flex-end; + gap: 16px; + + .button { + margin: 0; + padding: 0 16px; + } + + .sure { + color: @white; + } + } + + :global { + .anticon-exclamation-circle { + display: none; + } + } +} diff --git a/src/pages/inject/LevitateBall/app.tsx b/src/pages/inject/LevitateBall/app.tsx new file mode 100644 index 00000000..670ded0a --- /dev/null +++ b/src/pages/inject/LevitateBall/app.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect, useRef } from 'react'; +import Icon from '@ant-design/icons'; +import classnames from 'classnames'; +import { Button, Modal, Radio } from 'antd'; +import CloseCircle from '@/assets/svg/close-circle.svg'; +import YuqueLogoSve from '@/assets/svg/yuque-logo.svg'; +import { backgroundBridge } from '@/core/bridge/background'; +import { useForceUpdate } from '@/hooks/useForceUpdate'; +import { ILevitateConfig } from '@/isomorphic/constant/levitate'; +import { + LevitateBallMessageActions, + LevitateBallMessageKey, +} from '@/isomorphic/event/levitateBall'; +import LinkHelper from '@/isomorphic/link-helper'; +import AntdLayout from '@/components/AntdLayout'; +import styles from './app.module.less'; + +type DisableType = 'disableUrl' | 'disableOnce' | 'disableForever'; + +const url = `${window.location.origin}${window.location.pathname}`; + +function App() { + const [config, setConfig] = useState({} as ILevitateConfig); + const [isHover, setIsHover] = useState(false); + const [shortKey, setShortKey] = useState(''); + const [dragging, setDragging] = useState(false); + const { forceUpdate } = useForceUpdate(); + const entryStartActionRef = useRef<'click' | 'drag' | ''>(''); + const positionRef = useRef({ + top: 0, + }); + + const handleDrag = (event: React.DragEvent) => { + if (!dragging || !event.clientY) return; + positionRef.current.top = event.clientY; + forceUpdate(); + }; + + const disableLevitateBall = (type: DisableType) => { + switch (type) { + case 'disableOnce': { + break; + } + case 'disableUrl': { + const iconLink = + document.querySelector("link[rel*='icon']") || + document.querySelector("link[rel*='Icon']"); + const iconUrl = (iconLink as HTMLLinkElement)?.href; + backgroundBridge.configManager.update('levitate', 'disableUrl', [ + ...config.disableUrl, + { + origin: url, + icon: iconUrl, + }, + ]); + break; + } + case 'disableForever': { + backgroundBridge.configManager.update('levitate', 'enable', false, { + notice: true, + }); + break; + } + default: + break; + } + window._yuque_ext_app.removeLevitateBall(); + }; + + const onCloseLevitateBall = () => { + let disableType: DisableType = 'disableOnce'; + Modal.confirm({ + content: ( + + { + disableType = e.target.value; + }} + > + + {__i18n('本次关闭直到下次访问')} + + {__i18n('当前网站禁用')} + {__i18n('永久禁用')} + +
+ {__i18n('可在')} +
{ + backgroundBridge.tab.create(LinkHelper.sidePanelSettingPage); + }} + > + {__i18n('设置页')} +
+ {__i18n('开启')} +
+
+ ), + prefixCls: 'yuque-chrome-extension', + closable: true, + title: __i18n('关闭悬浮球'), + centered: true, + wrapClassName: styles.disableModal, + maskClosable: true, + footer: ( + +
+ + +
+
+ ), + }); + }; + + const handleDragEnd = (event: React.DragEvent) => { + positionRef.current.top = event.clientY; + setDragging(false); + entryStartActionRef.current = ''; + backgroundBridge.configManager.update( + 'levitate', + 'position', + positionRef.current.top, + ); + }; + + useEffect(() => { + backgroundBridge.user.getUserShortCut().then(res => { + setShortKey(res.openSidePanel || ''); + }); + backgroundBridge.configManager + .get('levitate') + .then((res: ILevitateConfig) => { + setConfig(res); + positionRef.current.top = parseInt(res.position); + }); + }, []); + + useEffect(() => { + const onMessage = (e: MessageEvent) => { + const { key, action, data } = e.data || {}; + if (key !== LevitateBallMessageKey) { + return; + } + switch (action) { + case LevitateBallMessageActions.levitateBallConfigUpdate: { + data && setConfig(data); + break; + } + default: + break; + } + }; + window.addEventListener('message', onMessage); + return () => { + window.removeEventListener('message', onMessage); + }; + }, []); + + if (!config.enable || config.disableUrl.find(item => item.origin === url)) { + return null; + } + + return ( + <> +
{ + setIsHover(false); + }} + > +
+ +
+
{ + entryStartActionRef.current = 'click'; + }} + onMouseMove={() => { + if (!entryStartActionRef.current) { + return; + } + entryStartActionRef.current = 'drag'; + setDragging(true); + }} + onMouseUp={() => { + if (entryStartActionRef.current === 'click') { + window._yuque_ext_app.showSidePanel(); + } + entryStartActionRef.current = ''; + }} + onMouseEnter={() => { + setIsHover(true); + }} + > +
+ +
{shortKey}
+
+
+
+ {dragging && ( +
{ + setDragging(false); + }} + /> + )} + + ); +} + +export default App; diff --git a/src/pages/inject/LevitateBall/index.tsx b/src/pages/inject/LevitateBall/index.tsx new file mode 100644 index 00000000..65c6267a --- /dev/null +++ b/src/pages/inject/LevitateBall/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import AntdLayout from '@/components/AntdLayout'; +import LevitateBallApp from './app'; + + +interface ICreateWordMarkOption { + dom: HTMLElement; +} + +function App() { + return ( + + + + ); +} + +export function createLevitateBall(option: ICreateWordMarkOption) { + const div = document.createElement('div'); + const root = createRoot(div); + root.render(); + option.dom.appendChild(div); + + return () => { + root.unmount(); + option.dom.removeChild(div); + }; +} diff --git a/src/pages/inject/WordMark/Inner/DisableMenu.tsx b/src/pages/inject/WordMark/Inner/DisableMenu.tsx index 47ffbed1..87a45be0 100644 --- a/src/pages/inject/WordMark/Inner/DisableMenu.tsx +++ b/src/pages/inject/WordMark/Inner/DisableMenu.tsx @@ -7,13 +7,19 @@ import styles from './DisableMenu.module.less'; function DisableMenu() { const disableForever = () => { - backgroundBridge.wordMarkConfig.update(WordMarkConfigKey.enable, false, { - notice: true, - }); + backgroundBridge.configManager.update( + 'wordMark', + WordMarkConfigKey.enable, + false, + { + notice: true, + }, + ); }; const disableForPage = () => { - backgroundBridge.wordMarkConfig.update( + backgroundBridge.configManager.update( + 'wordMark', WordMarkConfigKey.disableUrl, `${window.location.origin}${window.location.pathname}`, { diff --git a/src/pages/inject/WordMark/Inner/OperateMenu.tsx b/src/pages/inject/WordMark/Inner/OperateMenu.tsx index fd5270ef..a7491fcc 100644 --- a/src/pages/inject/WordMark/Inner/OperateMenu.tsx +++ b/src/pages/inject/WordMark/Inner/OperateMenu.tsx @@ -26,7 +26,11 @@ function OperateMenu(props: IOperateMenuProps) { const updateToolbar = (list: ToolbarItem[]) => { const result = list.map(item => item.id) as WordMarkOptionTypeEnum[]; setToolbarKeys(result); - backgroundBridge.wordMarkConfig.update(WordMarkConfigKey.toolbars, result); + backgroundBridge.configManager.update( + 'wordMark', + WordMarkConfigKey.toolbars, + result, + ); }; const toolbars = useMemo(() => { diff --git a/src/pages/inject/WordMark/Inner/index.tsx b/src/pages/inject/WordMark/Inner/index.tsx index ce62a0e8..65cc45b1 100644 --- a/src/pages/inject/WordMark/Inner/index.tsx +++ b/src/pages/inject/WordMark/Inner/index.tsx @@ -28,7 +28,8 @@ function InnerWordMark(props: InnerWordMarkProps) { const result = tools.includes(type) ? tools.filter(t => t !== type) : [type, ...tools]; - backgroundBridge.wordMarkConfig.update( + backgroundBridge.configManager.update( + 'wordMark', WordMarkConfigKey.innerPinList, result, ); diff --git a/src/pages/inject/action-listener.tsx b/src/pages/inject/action-listener.tsx index 1719c610..bdc8c1dc 100644 --- a/src/pages/inject/action-listener.tsx +++ b/src/pages/inject/action-listener.tsx @@ -13,6 +13,7 @@ import { AccountLayoutMessageActions } from '@/isomorphic/event/accountLayout'; import { App } from './content-scripts'; import { showScreenShot } from './ScreenShot'; import { showSelectArea } from './AreaSelector'; +import { LevitateBallMessageActions } from '@/isomorphic/event/levitateBall'; type MessageSender = chrome.runtime.MessageSender; @@ -115,6 +116,14 @@ export const initContentScriptActionListener = (context: App) => { sendResponse(true); break; } + case ContentScriptEvents.LevitateConfigChange: { + context.sendMessageToLevitateBall( + LevitateBallMessageActions.levitateBallConfigUpdate, + request.data, + ); + sendResponse(true); + break; + } case ContentScriptEvents.AddContentToClipAssistant: { context.sendMessageToClipAssistant( ClipAssistantMessageActions.addContent, diff --git a/src/pages/inject/content-scripts.ts b/src/pages/inject/content-scripts.ts index e46a9102..7cd5b86a 100644 --- a/src/pages/inject/content-scripts.ts +++ b/src/pages/inject/content-scripts.ts @@ -16,11 +16,16 @@ import { AccountLayoutMessageActions, AccountLayoutMessageKey, } from '@/isomorphic/event/accountLayout'; +import { + LevitateBallMessageActions, + LevitateBallMessageKey, +} from '@/isomorphic/event/levitateBall'; import { initContentScriptActionListener, initContentScriptMessageListener, } from './action-listener'; import { createWordMark } from './WordMark'; +import { createLevitateBall } from './LevitateBall'; import '@/styles/inject.less'; enum SidePanelStatus { @@ -66,6 +71,10 @@ export class App { // }; + public removeLevitateBall: VoidCallback = () => { + // + }; + constructor() { this.initRoot(); } @@ -104,6 +113,9 @@ export class App { initContentScriptActionListener(this); initContentScriptMessageListener(); this.initSidePanel(); + this.removeLevitateBall = createLevitateBall({ + dom: root, + }); }); } @@ -125,7 +137,7 @@ export class App { height: 100vh; right: 0; top: 0; - z-index: 999999; + z-index: 2147483645; color-scheme: none; user-select: none; } @@ -272,6 +284,20 @@ export class App { '*', ); } + + async sendMessageToLevitateBall( + action: LevitateBallMessageActions, + data?: any, + ) { + window.postMessage( + { + key: LevitateBallMessageKey, + action, + data, + }, + '*', + ); + } } function initSandbox() { diff --git a/src/pages/setting/app.tsx b/src/pages/setting/app.tsx index 5b0c6aba..b37652cb 100644 --- a/src/pages/setting/app.tsx +++ b/src/pages/setting/app.tsx @@ -9,6 +9,7 @@ import About from './about'; import Help from './help'; import Shortcut from './shortcut'; import styles from './app.module.less'; +import SidePanel from './sidePanel'; import '@/styles/global.less'; initI18N(); @@ -18,6 +19,8 @@ enum Page { general = 'general', // 快捷键设置 shortcut = 'shortcut', + // 侧边栏 + sidePanel = 'sidePanel', // 划词工具栏 wordMark = 'wordMark', // 帮助页面 @@ -37,6 +40,11 @@ const menus = [ key: Page.shortcut, page: , }, + { + name: __i18n('侧边栏'), + key: Page.sidePanel, + page: , + }, { name: __i18n('划词工具栏'), key: Page.wordMark, diff --git a/src/pages/setting/sidePanel/index.module.less b/src/pages/setting/sidePanel/index.module.less new file mode 100644 index 00000000..767d85a5 --- /dev/null +++ b/src/pages/setting/sidePanel/index.module.less @@ -0,0 +1,46 @@ +@import '~@/styles/parameters.less'; + +.configWrapper { + max-width: 668px; + + .card { + padding: 24px 0; + border-bottom: 1px solid @border-light-color; + color: @text-primary; + .body { + display: flex; + flex-direction: column; + gap: 12px; + + .configItem { + display: flex; + justify-content: space-between; + + :global { + .yuque-chrome-extension-select-arrow { + color: @text-color-secondary; + } + } + } + + .desc { + font-size: @font-size; + display: flex; + align-items: center; + } + + .disableUrlCard { + margin-top: 12px; + } + } + } + + .card:first-child { + padding-top: 0; + } + + .card:last-child { + border-bottom: 1px solid transparent; + } +} + diff --git a/src/pages/setting/sidePanel/index.tsx b/src/pages/setting/sidePanel/index.tsx new file mode 100644 index 00000000..4e185511 --- /dev/null +++ b/src/pages/setting/sidePanel/index.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Switch } from 'antd'; +import { backgroundBridge } from '@/core/bridge/background'; +import { + ILevitateConfig, + LevitateConfigKey, +} from '@/isomorphic/constant/levitate'; +import DisableUrlCard, { IDisableUrlItem } from '@/components/DisableUrlCard'; +import styles from './index.module.less'; + +function Shortcut() { + const [config, setConfig] = useState({} as ILevitateConfig); + + const onConfigChange = useCallback( + async (key: LevitateConfigKey, value: any) => { + await backgroundBridge.configManager.update('levitate', key, value, { + notice: true, + }); + setConfig(pre => ({ + ...pre, + [key]: value, + })); + }, + [], + ); + + const onDelete = useCallback( + (item: IDisableUrlItem, index: number) => { + const filterArray = config.disableUrl?.filter( + d => d.origin !== item.origin, + ); + onConfigChange('disableUrl', filterArray); + }, + [config], + ); + + useEffect(() => { + backgroundBridge.configManager.get('levitate').then(res => { + setConfig(res); + }); + }, []); + + return ( +
+
+
+
+
{__i18n('展示侧边栏悬浮气泡')}
+ onConfigChange('enable', !config.enable)} + /> +
+ {!!config.disableUrl?.length && ( +
+
+ {__i18n('管理不展示侧边栏气泡的页面')} +
+
+ +
+
+ )} +
+
+
+ ); +} + +export default React.memo(Shortcut); diff --git a/src/pages/setting/wordMark/index.tsx b/src/pages/setting/wordMark/index.tsx index 33ae08c0..ba46118c 100644 --- a/src/pages/setting/wordMark/index.tsx +++ b/src/pages/setting/wordMark/index.tsx @@ -57,7 +57,9 @@ function WordMark() { const [config, setConfig] = useState(null); const onConfigChange = async (key: WordMarkConfigKey, value: any) => { - await backgroundBridge.wordMarkConfig.update(key, value, { notice: true }); + await backgroundBridge.configManager.update('wordMark', key, value, { + notice: true, + }); setConfig({ ...(config as IWordMarkConfig), [key]: value, @@ -65,7 +67,7 @@ function WordMark() { }; useEffect(() => { - backgroundBridge.wordMarkConfig.get().then(res => { + backgroundBridge.configManager.get('wordMark').then(res => { setConfig(res); }); }, []); @@ -113,7 +115,8 @@ function WordMark() {
{__i18n('默认保存位置')}
{ - backgroundBridge.wordMarkConfig.update( + backgroundBridge.configManager.update( + 'wordMark', WordMarkConfigKey.defaultSavePosition, item, );