From 712ca2d56507c664461c09f58c5fc301155d916f Mon Sep 17 00:00:00 2001 From: t7yang Date: Tue, 30 Jul 2024 14:53:49 +0800 Subject: [PATCH] fix: try to fix the blank page on mobile #48 - deprecate the namespace approach - the some APIs optional in order to force us to check before use --- src/background/clipboard/index.ts | 4 +- src/background/menu/browser-action.ts | 22 ++--- src/background/runtime/handle-get-target.ts | 4 +- src/background/state/bg-state.ts | 5 +- src/background/state/mount-pref-listener.ts | 4 +- src/content/state.ts | 6 +- src/options/hooks/filter/index.ts | 8 +- src/options/hooks/general/use-general-opt.ts | 8 +- src/options/hooks/menu/index.ts | 16 ++-- src/options/hooks/word/use-word.ts | 6 +- src/options/pages/filter/FilterSettings.tsx | 4 +- src/options/pages/word/WordSettings.tsx | 4 +- src/service/browser-action/browser-action.ts | 7 -- src/service/browser-action/set-badge.ts | 6 +- src/service/commands/commands.ts | 6 -- src/service/downloads/downloads.ts | 5 -- src/service/menu/determine-context.ts | 15 ++-- src/service/menu/menus.ts | 26 +++--- src/service/notification/create-noti.ts | 6 +- src/service/notification/notifications.ts | 6 -- src/service/permissions/permissions.ts | 5 -- src/service/runtime/runtime.ts | 10 --- src/service/storage/export-pref.ts | 27 +++--- src/service/storage/import-pref.ts | 4 +- src/service/storage/local.ts | 5 +- src/service/storage/reset-pref.ts | 9 +- src/service/storage/storage.ts | 92 ++++++++++---------- src/service/tabs/detect-language.ts | 8 +- src/service/tabs/tabs.ts | 11 --- src/webextension-polyfill.d.ts | 17 ++++ tsconfig.json | 2 +- 31 files changed, 159 insertions(+), 199 deletions(-) delete mode 100644 src/service/browser-action/browser-action.ts delete mode 100644 src/service/commands/commands.ts delete mode 100644 src/service/downloads/downloads.ts delete mode 100644 src/service/notification/notifications.ts delete mode 100644 src/service/permissions/permissions.ts delete mode 100644 src/service/runtime/runtime.ts delete mode 100644 src/service/tabs/tabs.ts create mode 100644 src/webextension-polyfill.d.ts diff --git a/src/background/clipboard/index.ts b/src/background/clipboard/index.ts index 532f667..e6fd7ea 100644 --- a/src/background/clipboard/index.ts +++ b/src/background/clipboard/index.ts @@ -1,7 +1,7 @@ import { LangType } from 'tongwen-core'; +import { browser } from '../../service/browser'; import { i18n } from '../../service/i18n/i18n'; import { createNoti } from '../../service/notification/create-noti'; -import { permissions } from '../../service/permissions/permissions'; import { BgState } from '../state'; const convertClipboardContent = (state: BgState, target: LangType): Promise => @@ -11,7 +11,7 @@ const convertClipboardContent = (state: BgState, target: LangType): Promise navigator.clipboard.writeText(text)); export const convertClipboard = (state: BgState, target: LangType): Promise => - permissions + browser.permissions .request({ permissions: ['clipboardRead', 'clipboardWrite'] }) .then(async isGet => (isGet && (await convertClipboardContent(state, target)), isGet)) .then(isGet => { diff --git a/src/background/menu/browser-action.ts b/src/background/menu/browser-action.ts index 288cc26..9a8eda4 100644 --- a/src/background/menu/browser-action.ts +++ b/src/background/menu/browser-action.ts @@ -1,18 +1,15 @@ import { LangType } from 'tongwen-core'; -import { Menus } from 'webextension-polyfill'; import { isUrlLike } from '../../preference/filter-rule'; import { FilterTarget } from '../../preference/types/v2'; +import { browser } from '../../service/browser'; import { i18n } from '../../service/i18n/i18n'; -import { menus } from '../../service/menu/menus'; -import { runtime } from '../../service/runtime/runtime'; import { addFilterRule } from '../../service/storage/local'; -import { tabs } from '../../service/tabs/tabs'; import { getHostName, getRandomId } from '../../utilities'; import { convertClipboard } from '../clipboard'; import { BgState } from '../state'; // TODO: handle for none http protocol url -type AddDomainToRule = (t: FilterTarget) => (i: menus.OnClickData, t: tabs.Tab) => void; +type AddDomainToRule = (t: FilterTarget) => (i: browser.Menus.OnClickData, t: browser.Tabs.Tab) => void; const addDomainToRules: AddDomainToRule = target => (_, tab) => { isUrlLike(tab.url!) && addFilterRule({ @@ -23,7 +20,7 @@ const addDomainToRules: AddDomainToRule = target => (_, tab) => { }); }; -const createBrowserActionProperties: () => menus.CreateProperties[] = () => [ +const createBrowserActionProperties: () => browser.Menus.CreateCreatePropertiesType[] = () => [ { title: i18n.getMessage('MSG_ADD_DOMAIN_TO_DISABLED'), onclick: addDomainToRules('disabled'), @@ -38,13 +35,13 @@ const createBrowserActionProperties: () => menus.CreateProperties[] = () => [ }, { title: i18n.getMessage('MSG_OPTION'), - onclick: () => runtime.openOptionsPage(), + onclick: () => browser.runtime.openOptionsPage(), }, ]; const reqConvertClipboard = (state: BgState, target: LangType) => () => void convertClipboard(state, target); -const createClipboardProperties: (s: BgState) => menus.CreateProperties[] = state => [ +const createClipboardProperties: (s: BgState) => browser.Menus.CreateCreatePropertiesType[] = state => [ { title: i18n.getMessage('MSG_CONVERT_CLIPBOARD_S2T'), onclick: reqConvertClipboard(state, LangType.s2t), @@ -57,12 +54,15 @@ const createClipboardProperties: (s: BgState) => menus.CreateProperties[] = stat // TODO: need icon export async function createBrowserActionMenus(state: BgState): Promise<(string | number)[]> { - const browserActionMenuItems: menus.CreateProperties[] = [ + const browserActionMenuItems: browser.Menus.CreateCreatePropertiesType[] = [ ...createBrowserActionProperties(), ...createClipboardProperties(state), ].map(item => - Object.assign(item, { type: 'normal', contexts: ['browser_action'] } satisfies Menus.CreateCreatePropertiesType), + Object.assign(item, { + type: 'normal', + contexts: ['browser_action'], + } satisfies browser.Menus.CreateCreatePropertiesType), ); - return Promise.all(browserActionMenuItems.map(item => menus.create(item))); + return Promise.all(browserActionMenuItems.map(item => browser.menus.create(item))); } diff --git a/src/background/runtime/handle-get-target.ts b/src/background/runtime/handle-get-target.ts index 06ab96f..f012680 100644 --- a/src/background/runtime/handle-get-target.ts +++ b/src/background/runtime/handle-get-target.ts @@ -1,12 +1,12 @@ -import { Runtime } from 'webextension-polyfill'; import { MaybeTransTarget } from '../../preference/types/types'; import { FilterTarget } from '../../preference/types/v2'; +import type { browser } from '../../service/browser'; import { BgState } from '../state'; import { getTargetByAutoConvert } from './handle-get-auto-convert'; import { getTargetByFilter } from './handle-get-filter-target'; // TODO: remove type assertion after Promise.then type infer bug fixed -type GetTarget = (s: BgState, tab: Runtime.MessageSender) => Promise; +type GetTarget = (s: BgState, tab: browser.Runtime.MessageSender) => Promise; export const getTarget: GetTarget = (state, sender) => Promise.resolve(getTargetByFilter(state, sender.url!)) .then( diff --git a/src/background/state/bg-state.ts b/src/background/state/bg-state.ts index d47e9b5..d65085f 100644 --- a/src/background/state/bg-state.ts +++ b/src/background/state/bg-state.ts @@ -2,7 +2,7 @@ import { Converter } from 'tongwen-core'; import { patchRulesRegExp } from '../../preference/filter-rule'; import { Pref } from '../../preference/types/lastest'; import { MenuId } from '../../service/menu/create-menu'; -import { storage } from '../../service/storage/storage'; +import { initialStorage } from '../../service/storage/storage'; import { Logger, loggerWith } from '../../utilities'; import { getConverter } from '../converter'; @@ -21,7 +21,6 @@ export const updateLogger = (state: BgState) => { }; export const createBgState = async (): Promise => - storage - .initial() + initialStorage() .then(pref => Promise.all([patchRegExp(pref), getConverter(pref.word)])) .then(([pref, converter]) => ({ pref, converter, logger: loggerWith(pref.general.debugMode) })); diff --git a/src/background/state/mount-pref-listener.ts b/src/background/state/mount-pref-listener.ts index 1b23fb4..8d4dd2a 100644 --- a/src/background/state/mount-pref-listener.ts +++ b/src/background/state/mount-pref-listener.ts @@ -4,11 +4,11 @@ import { Pref } from '../../preference/types/lastest'; import { PrefFilter } from '../../preference/types/v2'; import { setBadge } from '../../service/browser-action/set-badge'; import { createMenu } from '../../service/menu/create-menu'; -import { storage } from '../../service/storage/storage'; +import { listenStorage } from '../../service/storage/storage'; import { getConverter } from '../converter'; export function mountPrefListener(state: BgState) { - storage.listen( + listenStorage( changes => { state.logger('[BG_RECEIVE_SYNC_PREF_CHANGE]', changes); diff --git a/src/content/state.ts b/src/content/state.ts index 5f3c658..ccff559 100644 --- a/src/content/state.ts +++ b/src/content/state.ts @@ -1,5 +1,5 @@ import { TARGET_NODE_ATTRIBUTES } from 'tongwen-core'; -import { storage } from '../service/storage/storage'; +import { getStorage } from '../service/storage/storage'; import { ZhType } from '../service/tabs/tabs.constant'; import { getDetectLanguage } from './services'; @@ -22,8 +22,8 @@ export interface CtState { converting: Promise; } -const getUpdateLangAttr = () => storage.get('general').then(({ general }) => general.updateLangAttr); -const getDebugMode = () => storage.get('general').then(({ general }) => general.debugMode); +const getUpdateLangAttr = () => getStorage('general').then(({ general }) => general.updateLangAttr); +const getDebugMode = () => getStorage('general').then(({ general }) => general.debugMode); export async function createCtState(): Promise { return Promise.all([getDetectLanguage(), getUpdateLangAttr(), getDebugMode()]).then( diff --git a/src/options/hooks/filter/index.ts b/src/options/hooks/filter/index.ts index 70a501e..216e772 100644 --- a/src/options/hooks/filter/index.ts +++ b/src/options/hooks/filter/index.ts @@ -1,7 +1,7 @@ import { Reducer, useEffect, useReducer, useState } from 'react'; import { getDefaultPref } from '../../../preference/default'; import { PrefFilterRule } from '../../../preference/types/v2'; -import { storage } from '../../../service/storage/storage'; +import { getStorage } from '../../../service/storage/storage'; export type UseFilterRuleAction = | { type: 'DELETE'; payload: PrefFilterRule } @@ -43,9 +43,9 @@ export const useFilter = () => { const { rules, setRules } = useFilterRules(getDefaultPref().filter.rules); useEffect(() => { - storage - .get('filter') - .then(({ filter: { enabled, rules } }) => void (setEnable(enabled), setRules({ type: 'RESET', payload: rules }))); + getStorage('filter').then( + ({ filter: { enabled, rules } }) => void (setEnable(enabled), setRules({ type: 'RESET', payload: rules })), + ); }, []); return { enabled, setEnable, rules, setRules }; diff --git a/src/options/hooks/general/use-general-opt.ts b/src/options/hooks/general/use-general-opt.ts index ded1edc..1aff061 100644 --- a/src/options/hooks/general/use-general-opt.ts +++ b/src/options/hooks/general/use-general-opt.ts @@ -2,13 +2,13 @@ import { ChangeEventHandler, useEffect, useState } from 'react'; import { LangType } from 'tongwen-core'; import { getDefaultPref } from '../../../preference/default'; import { AutoConvertOpt, BrowserActionOpt, PrefGeneral } from '../../../preference/types/v2'; -import { storage } from '../../../service/storage/storage'; +import { getStorage, listenStorage, setStorage } from '../../../service/storage/storage'; export const useGeneralOpt = () => { const [general, set] = useState(getDefaultPref().general); const setGeneral = (key: T, value: PrefGeneral[T]) => - storage.set({ general: { ...general, [key]: value } }); + setStorage({ general: { ...general, [key]: value } }); const setAutoConvert: ChangeEventHandler = e => void setGeneral('autoConvert', e.currentTarget.value as AutoConvertOpt); const setBrowserAction: ChangeEventHandler = e => @@ -22,7 +22,7 @@ export const useGeneralOpt = () => { useEffect( () => - storage.listen(changes => changes.general?.newValue && set(changes.general?.newValue), { + listenStorage(changes => changes.general?.newValue && set(changes.general?.newValue), { keys: ['general'], areaName: ['local'], }), @@ -30,7 +30,7 @@ export const useGeneralOpt = () => { ); useEffect(() => { - storage.get('general').then(({ general }) => set(general)); + getStorage('general').then(({ general }) => set(general)); }, []); return { diff --git a/src/options/hooks/menu/index.ts b/src/options/hooks/menu/index.ts index 1fe234e..f0c1307 100644 --- a/src/options/hooks/menu/index.ts +++ b/src/options/hooks/menu/index.ts @@ -1,46 +1,46 @@ import { ChangeEventHandler, useEffect, useState } from 'react'; import { LangType } from 'tongwen-core'; import { getDefaultPref } from '../../../preference/default'; -import { storage } from '../../../service/storage/storage'; +import { getStorage, listenStorage, setStorage } from '../../../service/storage/storage'; export const useMenu = () => { const [menu, set] = useState(getDefaultPref().menu); const setMenuEnable: ChangeEventHandler = e => - storage.set({ menu: { ...menu, enabled: e.currentTarget.checked } }); + setStorage({ menu: { ...menu, enabled: e.currentTarget.checked } }); const setWebS2t: ChangeEventHandler = e => - storage.set({ + setStorage({ menu: { ...menu, group: { ...menu.group, webpage: { ...menu.group.webpage, [LangType.s2t]: e.currentTarget.checked } }, }, }); const setWebT2s: ChangeEventHandler = e => - storage.set({ + setStorage({ menu: { ...menu, group: { ...menu.group, webpage: { ...menu.group.webpage, [LangType.t2s]: e.currentTarget.checked } }, }, }); const setTextS2t: ChangeEventHandler = e => - storage.set({ + setStorage({ menu: { ...menu, group: { ...menu.group, textarea: { ...menu.group.textarea, [LangType.s2t]: e.currentTarget.checked } }, }, }); const setTextT2s: ChangeEventHandler = e => - storage.set({ + setStorage({ menu: { ...menu, group: { ...menu.group, textarea: { ...menu.group.textarea, [LangType.t2s]: e.currentTarget.checked } }, }, }); - useEffect(() => storage.listen(changes => set(changes.menu?.newValue), { keys: ['menu'], areaName: ['local'] }), []); + useEffect(() => listenStorage(changes => set(changes.menu?.newValue), { keys: ['menu'], areaName: ['local'] }), []); useEffect(() => { - storage.get('menu').then(({ menu }) => set(menu)); + getStorage('menu').then(({ menu }) => set(menu)); }, []); return { menu, setMenuEnable, setWebS2t, setWebT2s, setTextS2t, setTextT2s }; diff --git a/src/options/hooks/word/use-word.ts b/src/options/hooks/word/use-word.ts index a7eafcf..f51588b 100644 --- a/src/options/hooks/word/use-word.ts +++ b/src/options/hooks/word/use-word.ts @@ -1,16 +1,16 @@ import { useEffect, useState } from 'react'; import { getDefaultPref } from '../../../preference/default'; import { PrefWord } from '../../../preference/types/v2'; -import { storage } from '../../../service/storage/storage'; +import { getStorage, listenStorage } from '../../../service/storage/storage'; export const useWord = () => { const [word, setWord] = useState(getDefaultPref().word); - storage.listen(({ word }) => setWord(word?.newValue), { keys: ['word'], areaName: ['local'] }); + listenStorage(({ word }) => setWord(word?.newValue), { keys: ['word'], areaName: ['local'] }); useEffect( () => - void storage.get('word').then(({ word }) => { + void getStorage('word').then(({ word }) => { setWord(word); }), [], diff --git a/src/options/pages/filter/FilterSettings.tsx b/src/options/pages/filter/FilterSettings.tsx index f83bee1..dde7c18 100644 --- a/src/options/pages/filter/FilterSettings.tsx +++ b/src/options/pages/filter/FilterSettings.tsx @@ -3,7 +3,7 @@ import { createFilterRule } from '../../../preference/filter-rule'; import { PrefFilterRule } from '../../../preference/types/v2'; import { i18n } from '../../../service/i18n/i18n'; import { createNoti } from '../../../service/notification/create-noti'; -import { storage } from '../../../service/storage/storage'; +import { setStorage } from '../../../service/storage/storage'; import { Button, Checkbox, Modal } from '../../components'; import { useFilter } from '../../hooks/filter'; import { useToggle } from '../../hooks/state/use-toggle'; @@ -47,7 +47,7 @@ export const FilterSettings: FC = () => { ); const save = useCallback( - () => storage.set({ filter: { enabled, rules } }).then(() => createNoti(i18n.getMessage('MSG_UPDATE_COMPLETED'))), + () => setStorage({ filter: { enabled, rules } }).then(() => createNoti(i18n.getMessage('MSG_UPDATE_COMPLETED'))), [enabled, rules], ); diff --git a/src/options/pages/word/WordSettings.tsx b/src/options/pages/word/WordSettings.tsx index b4a29da..8687e98 100644 --- a/src/options/pages/word/WordSettings.tsx +++ b/src/options/pages/word/WordSettings.tsx @@ -3,7 +3,7 @@ import { LangType } from 'tongwen-core'; import { PrefWordDefault } from '../../../preference/types/v2'; import { i18n } from '../../../service/i18n/i18n'; import { createNoti } from '../../../service/notification/create-noti'; -import { storage } from '../../../service/storage/storage'; +import { setStorage } from '../../../service/storage/storage'; import { Button, Divider, Modal } from '../../components'; import { useToggle } from '../../hooks/state/use-toggle'; import { useWord } from '../../hooks/word/use-word'; @@ -62,7 +62,7 @@ export const WordSettings: FC = () => { ); const save = useCallback( - () => storage.set({ word }).then(() => createNoti(i18n.getMessage('MSG_UPDATE_COMPLETED'))), + () => setStorage({ word }).then(() => createNoti(i18n.getMessage('MSG_UPDATE_COMPLETED'))), [word], ); diff --git a/src/service/browser-action/browser-action.ts b/src/service/browser-action/browser-action.ts deleted file mode 100644 index 94a1064..0000000 --- a/src/service/browser-action/browser-action.ts +++ /dev/null @@ -1,7 +0,0 @@ -import browser from 'webextension-polyfill'; - -export namespace browserAction { - export const setBadgeText = browser.browserAction.setBadgeText; - export const setBadgeBackgroundColor = browser.browserAction.setBadgeBackgroundColor; - export const onClicked = browser.browserAction.onClicked; -} diff --git a/src/service/browser-action/set-badge.ts b/src/service/browser-action/set-badge.ts index 3e19e95..3ca4e89 100644 --- a/src/service/browser-action/set-badge.ts +++ b/src/service/browser-action/set-badge.ts @@ -1,7 +1,7 @@ import { LangType } from 'tongwen-core'; import { Pref } from '../../preference/types/lastest'; import { BrowserActionOpt } from '../../preference/types/v2'; -import { browserAction } from './browser-action'; +import { browser } from '../browser'; const browserActionToBadge = (ba: BrowserActionOpt) => (ba === 'auto' ? 'A' : ba === LangType.s2t ? 'T' : 'S'); @@ -9,8 +9,8 @@ const color = '#C0C0C0'; export const setBadge = (pref: Pref) => { const text = browserActionToBadge(pref.general.browserAction); - const setText = browserAction.setBadgeText({ text }); - const setBg = browserAction.setBadgeBackgroundColor({ color }); + const setText = browser.browserAction.setBadgeText({ text }); + const setBg = browser.browserAction.setBadgeBackgroundColor({ color }); return Promise.all([setText, setBg]); }; diff --git a/src/service/commands/commands.ts b/src/service/commands/commands.ts deleted file mode 100644 index 46e98bd..0000000 --- a/src/service/commands/commands.ts +++ /dev/null @@ -1,6 +0,0 @@ -import browser, { Events } from 'webextension-polyfill'; -import { CommandType } from './type'; - -export namespace commands { - export const onCommand = browser.commands.onCommand as Events.Event<(command: CommandType) => void>; -} diff --git a/src/service/downloads/downloads.ts b/src/service/downloads/downloads.ts deleted file mode 100644 index bc3dfbe..0000000 --- a/src/service/downloads/downloads.ts +++ /dev/null @@ -1,5 +0,0 @@ -import browser from 'webextension-polyfill'; - -export namespace downloads { - export const download = browser.downloads.download; -} diff --git a/src/service/menu/determine-context.ts b/src/service/menu/determine-context.ts index 4bc4249..803b982 100644 --- a/src/service/menu/determine-context.ts +++ b/src/service/menu/determine-context.ts @@ -1,21 +1,22 @@ import { PrefMenuGroup, PrefMenuGroupKeys, PrefMenuOptions } from '../../preference/types/v2'; -import { menus } from './menus'; +import type { browser } from '../browser'; +import { ContextOnAll, ContextOnEditable } from './menus'; const hasEnabled = (options: PrefMenuOptions) => options.s2t || options.t2s; -export function getSubMenuContexts(prefKey: PrefMenuGroupKeys): menus.ContextType[] { - return prefKey === 'textarea' ? menus.ContextOnEditable : menus.ContextOnAll; +export function getSubMenuContexts(prefKey: PrefMenuGroupKeys): browser.Menus.ContextType[] { + return prefKey === 'textarea' ? ContextOnEditable : ContextOnAll; } -export function getTopMenuContexts({ textarea, webpage }: PrefMenuGroup): menus.ContextType[] { +export function getTopMenuContexts({ textarea, webpage }: PrefMenuGroup): browser.Menus.ContextType[] { const hasEditable = hasEnabled(textarea); const hasOther = hasEnabled(webpage); return hasEditable && hasOther - ? [...menus.ContextOnAll, ...menus.ContextOnEditable] + ? [...ContextOnAll, ...ContextOnEditable] : hasEditable - ? menus.ContextOnEditable + ? ContextOnEditable : hasOther - ? menus.ContextOnAll + ? ContextOnAll : []; } diff --git a/src/service/menu/menus.ts b/src/service/menu/menus.ts index 7a948a1..dd8f3bb 100644 --- a/src/service/menu/menus.ts +++ b/src/service/menu/menus.ts @@ -1,16 +1,12 @@ -import browser, { Menus } from 'webextension-polyfill'; +import browser from 'webextension-polyfill'; -export namespace menus { - export const create = browser.menus.create; - export const remove = browser.menus.remove; - - // constant - export const ContextOnAll: ContextType[] = ['page', 'frame', 'selection', 'link', 'image', 'video', 'audio']; - export const ContextOnEditable: ContextType[] = ['editable']; - - // types - export type CreateProperties = Parameters[0]; - export type OnClickData = Menus.OnClickData; - export type ContextType = Menus.ContextType; - export type ItemType = Menus.ItemType; -} +export const ContextOnAll: browser.Menus.ContextType[] = [ + 'page', + 'frame', + 'selection', + 'link', + 'image', + 'video', + 'audio', +]; +export const ContextOnEditable: browser.Menus.ContextType[] = ['editable']; diff --git a/src/service/notification/create-noti.ts b/src/service/notification/create-noti.ts index 9f72ba1..a151a9a 100644 --- a/src/service/notification/create-noti.ts +++ b/src/service/notification/create-noti.ts @@ -1,14 +1,14 @@ import { getRandomId } from '../../utilities'; +import { browser } from '../browser'; import { i18n } from '../i18n/i18n'; -import { notifications } from './notifications'; -const autoDeleteNoti = (id: string, closeIn: number) => setTimeout(() => notifications.clear(id), closeIn); +const autoDeleteNoti = (id: string, closeIn: number) => setTimeout(() => browser.notifications.clear(id), closeIn); export const createNoti = (message: string, closeIn = 5000, id = getRandomId()) => { autoDeleteNoti(id, closeIn); // TODO: need i18n - return notifications.create(id, { + return browser.notifications.create(id, { type: 'basic', title: i18n.getMessage('NT_TITLE'), message, diff --git a/src/service/notification/notifications.ts b/src/service/notification/notifications.ts deleted file mode 100644 index 2ff4a90..0000000 --- a/src/service/notification/notifications.ts +++ /dev/null @@ -1,6 +0,0 @@ -import browser from 'webextension-polyfill'; - -export namespace notifications { - export const clear = browser.notifications.clear; - export const create = browser.notifications.create; -} diff --git a/src/service/permissions/permissions.ts b/src/service/permissions/permissions.ts deleted file mode 100644 index e454cb0..0000000 --- a/src/service/permissions/permissions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import browser from 'webextension-polyfill'; - -export namespace permissions { - export const request = browser.permissions.request; -} diff --git a/src/service/runtime/runtime.ts b/src/service/runtime/runtime.ts deleted file mode 100644 index d38c3a9..0000000 --- a/src/service/runtime/runtime.ts +++ /dev/null @@ -1,10 +0,0 @@ -import browser, { Runtime } from 'webextension-polyfill'; - -export namespace runtime { - export const openOptionsPage = browser.runtime.openOptionsPage; - export const onMessage = browser.runtime.onMessage; - export const sendMessage = browser.runtime.sendMessage; - - // types - export type MessageSender = Runtime.MessageSender; -} diff --git a/src/service/storage/export-pref.ts b/src/service/storage/export-pref.ts index 2664cc3..f6df12d 100644 --- a/src/service/storage/export-pref.ts +++ b/src/service/storage/export-pref.ts @@ -1,19 +1,22 @@ import { safeUpgradePref } from '../../preference/upgrade'; -import { downloads } from '../downloads/downloads'; +import { browser } from '../browser'; import { i18n } from '../i18n/i18n'; import { createNoti } from '../notification/create-noti'; import { BROWSER_TYPE } from '../types'; -import { storage } from './storage'; +import { getStorage } from './storage'; const delayRevoke = (url: string) => setTimeout(() => URL.revokeObjectURL(url), 60000); -export const exportPref = () => - storage - .get() - .then(pref => safeUpgradePref(BROWSER_TYPE, pref)) - .then(pref => new Blob([JSON.stringify(pref, null, 2)], { type: 'application/json;charset=utf-8' })) - .then(blob => URL.createObjectURL(blob)) - .then(url => (delayRevoke(url), url)) - .then(url => ({ url, filename: 'tongwentang-pref.json', saveAs: true })) - .then(downloads.download) - .catch(() => createNoti(i18n.getMessage('MSG_EXPORT_FAILED'))); +export const exportPref = () => { + // TODO: maybe we can optionally get the download permission here + return browser.downloads + ? getStorage() + .then(pref => safeUpgradePref(BROWSER_TYPE, pref)) + .then(pref => new Blob([JSON.stringify(pref, null, 2)], { type: 'application/json;charset=utf-8' })) + .then(blob => URL.createObjectURL(blob)) + .then(url => (delayRevoke(url), url)) + .then(url => ({ url, filename: 'tongwentang-pref.json', saveAs: true })) + .then(browser.downloads.download) + .catch(() => createNoti(i18n.getMessage('MSG_EXPORT_FAILED'))) + : Promise.resolve(); +}; diff --git a/src/service/storage/import-pref.ts b/src/service/storage/import-pref.ts index 433c888..42b6bb3 100644 --- a/src/service/storage/import-pref.ts +++ b/src/service/storage/import-pref.ts @@ -3,7 +3,7 @@ import { safeUpgradePref, validatePref } from '../../preference/upgrade'; import { i18n } from '../i18n/i18n'; import { createNoti } from '../notification/create-noti'; import { BrowserType } from '../types'; -import { storage } from './storage'; +import { setStorage } from './storage'; const parseJson = (raw: string) => Promise.resolve(JSON.parse(raw)).catch(() => Promise.reject(i18n.getMessage('MSG_JSON_ERROR'))); @@ -21,6 +21,6 @@ export const importPref = (type: BrowserType, raw: string): Promise => parseJson(raw) .then(validatePref(type)) .then(getValidPref(type)) - .then(storage.set) + .then(setStorage) .then(() => createNoti(i18n.getMessage('MSG_IMPORT_COMPLETED'))) .catch(() => createNoti(i18n.getMessage('MSG_IMPORT_FAILED'))); diff --git a/src/service/storage/local.ts b/src/service/storage/local.ts index 5cbf2b2..c3a5fde 100644 --- a/src/service/storage/local.ts +++ b/src/service/storage/local.ts @@ -1,10 +1,9 @@ import { Pref } from '../../preference/types/lastest'; import { PrefFilterRule } from '../../preference/types/v2'; -import { storage } from './storage'; +import { getStorage, setStorage } from './storage'; export type StoreReducer = (store: Pref) => Partial; -export const patchLocalStorage = (reducer: StoreReducer): Promise => - storage.get().then(reducer).then(storage.set); +export const patchLocalStorage = (reducer: StoreReducer): Promise => getStorage().then(reducer).then(setStorage); export const addFilterRule = (rule: PrefFilterRule): Promise => { return patchLocalStorage(({ filter: { rules, ...rest } }) => ({ diff --git a/src/service/storage/reset-pref.ts b/src/service/storage/reset-pref.ts index 283fb58..c875b23 100644 --- a/src/service/storage/reset-pref.ts +++ b/src/service/storage/reset-pref.ts @@ -1,25 +1,24 @@ import { getDefaultPref } from '../../preference/default'; import { i18n } from '../i18n/i18n'; import { createNoti } from '../notification/create-noti'; -import { storage } from './storage'; +import { getStorage, resetStorage, setStorage } from './storage'; const confirmReset = (msg: string) => confirm(msg); export const confirmResetPref = async (): Promise => confirmReset(i18n.getMessage('MSG_CONFIRM_RESET_ALL')) - ? storage - .reset() + ? resetStorage() .then(() => void createNoti(i18n.getMessage('MSG_PREF_RESET_COMPLETED'))) .catch(() => void createNoti(i18n.getMessage('MSG_PREF_RESET_FAILED'))) : undefined; -const extractCustom = () => storage.get().then(({ word: { custom } }) => custom); +const extractCustom = () => getStorage().then(({ word: { custom } }) => custom); export const confirmResetPrefKeep = async (): Promise => confirmReset(i18n.getMessage('MSG_CONFIRM_RESET')) ? extractCustom() .then(custom => (pref => ((pref.word.custom = custom), pref))(getDefaultPref())) - .then(storage.set) + .then(setStorage) .then(() => void createNoti(i18n.getMessage('MSG_PREF_RESET_COMPLETED'))) .catch(() => void createNoti(i18n.getMessage('MSG_PREF_RESET_FAILED'))) : undefined; diff --git a/src/service/storage/storage.ts b/src/service/storage/storage.ts index 4bcb738..b72385f 100644 --- a/src/service/storage/storage.ts +++ b/src/service/storage/storage.ts @@ -4,54 +4,50 @@ import { Pref, PrefKeys, PrefPick } from '../../preference/types/lastest'; import { safeUpgradePref, validatePref } from '../../preference/upgrade'; import { BROWSER_TYPE } from '../types'; -export namespace storage { - const local = browser.storage.local; - const onChanged = browser.storage.onChanged; - - // types - export type StorageAreaName = 'local' | 'managed' | 'sync'; - export type StorageChange = Storage.StorageChange; - export type StorageChanges = Partial<{ [P in PrefKeys]: storage.StorageChange }>; - export type StorageListener = (store: StorageChanges, areaName: A) => void; - - // custom - const updatePrefTime = (pref: Partial): Partial => ({ ...pref, meta: { update: Date.now() } }); - - export const get = (keys?: T): Promise> => { - return local.get(keys) as any; - }; - - export const set = (data: Partial): Promise => local.set(updatePrefTime(data)); - - /** - * reset pref, if undefined or null given, reset to default pref - */ - export const reset = (pref?: Pref) => local.clear().then(() => local.set(pref || getDefaultPref())); - - export const listen = ( - listener: StorageListener, - opt: Partial<{ keys: PKey[]; areaName: AreaName[] }> = {}, - ): (() => void) => { - const wrapper = (changes: StorageChanges, areaName: StorageAreaName) => { - opt.areaName && !opt.areaName.includes(areaName as any) - ? null - : !Array.isArray(opt.keys) +type StorageAreaName = Exclude; +type StorageChanges = Partial<{ [P in PrefKeys]: Storage.StorageChange }>; +type StorageListener = (store: StorageChanges, areaName: A) => void; + +const updatePrefTime = (pref: Partial): Partial => ({ ...pref, meta: { update: Date.now() } }); + +export const getStorage = ( + keys?: T, +): Promise> => { + return browser.storage.local.get(keys) as any; +}; + +export const setStorage = (data: Partial): Promise => browser.storage.local.set(updatePrefTime(data)); + +/** + * reset pref, if undefined or null given, reset to default pref + */ +export const resetStorage = (pref?: Pref) => { + return browser.storage.local.clear().then(() => browser.storage.local.set(pref || getDefaultPref())); +}; + +export const listenStorage = ( + listener: StorageListener, + opt: Partial<{ keys: PKey[]; areaName: AreaName[] }> = {}, +): (() => void) => { + const wrapper = (changes: StorageChanges, areaName: StorageAreaName) => { + opt.areaName && !opt.areaName.includes(areaName as any) + ? null + : !Array.isArray(opt.keys) + ? listener(changes, areaName as any) + : Object.keys(changes).some(key => opt.keys?.includes(key as PKey)) ? listener(changes, areaName as any) - : Object.keys(changes).some(key => opt.keys?.includes(key as PKey)) - ? listener(changes, areaName as any) - : null; - }; - - onChanged.addListener(wrapper as any); - return () => onChanged.removeListener(wrapper as any); + : null; }; - export const initial = async (): Promise => { - return local - .get() - .then(validatePref(BROWSER_TYPE)) - .then(async holder => (holder.invalid && (await local.clear()), holder.value())) - .then(pref => safeUpgradePref(BROWSER_TYPE, pref)) - .then(async pref => (await local.set(pref), pref)); - }; -} + browser.storage.onChanged.addListener(wrapper as any); + return () => browser.storage.onChanged.removeListener(wrapper as any); +}; + +export const initialStorage = async (): Promise => { + return browser.storage.local + .get() + .then(validatePref(BROWSER_TYPE)) + .then(async holder => (holder.invalid && (await browser.storage.local.clear()), holder.value())) + .then(pref => safeUpgradePref(BROWSER_TYPE, pref)) + .then(async pref => (await browser.storage.local.set(pref), pref)); +}; diff --git a/src/service/tabs/detect-language.ts b/src/service/tabs/detect-language.ts index 9ae9716..3cb0b1d 100644 --- a/src/service/tabs/detect-language.ts +++ b/src/service/tabs/detect-language.ts @@ -1,13 +1,13 @@ -import { tabs } from './tabs'; +import { browser } from '../browser'; import { chsTypes, chtTypes, ZhType } from './tabs.constant'; const langToZhtype = (lang: string): ZhType => chsTypes.find(tag => tag === lang) ? ZhType.hans : chtTypes.find(tag => tag === lang) ? ZhType.hant : ZhType.und; export const detectLanguage = (tabId?: number): Promise => - tabs - .detectLanguage(tabId) + browser.tabs + .detectLanguage?.(tabId) .then(lang => lang.toLowerCase()) .then(langToZhtype) // INFO: some browsers may fail with detect language api - .catch(() => ZhType.und); + .catch(() => ZhType.und) || Promise.resolve(ZhType.und); diff --git a/src/service/tabs/tabs.ts b/src/service/tabs/tabs.ts deleted file mode 100644 index 175b6d1..0000000 --- a/src/service/tabs/tabs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import browser, { Tabs } from 'webextension-polyfill'; - -export namespace tabs { - export const sendMessage = browser.tabs.sendMessage; - export const detectLanguage = browser.tabs.detectLanguage; - export const query = browser.tabs.query; - export const get = browser.tabs.get; - - // types - export type Tab = Tabs.Tab; -} diff --git a/src/webextension-polyfill.d.ts b/src/webextension-polyfill.d.ts new file mode 100644 index 0000000..64ae2cd --- /dev/null +++ b/src/webextension-polyfill.d.ts @@ -0,0 +1,17 @@ +import { type CommandType } from './service/commands/type'; + +declare module 'webextension-polyfill' { + namespace Browser { + // NOTE: mix with `undefined` is because these API may not exist on the mobile runtime + const commands: + | (Omit & { + onCommand: Events.Event<(command: CommandType, tab: Tabs.Tab | undefined) => void>; + }) + | undefined; + const contextMenus: ContextMenus.Static | undefined; + const downloads: Downloads.Static | undefined; + const tabs: Omit & { detectLanguage?: Tabs.Static['detectLanguage'] }; + } + + export = Browser; +} diff --git a/tsconfig.json b/tsconfig.json index c0d3765..b835da2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/**.jsx"], + "include": ["src"], "exclude": ["**/node_modules", "**/.*/", "src/**/*.test.ts"] }