diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index 665963661..000000000 --- a/global.d.ts +++ /dev/null @@ -1,559 +0,0 @@ - -/** - * The options - * - * @since 0.3.0 - */ -declare namespace timer { - namespace option { - type PopupDuration = - | "today" | "thisWeek" | "thisMonth" - | "last30Days" - /** - * Options used for the popup page - */ - type PopupOption = { - /** - * The max count of today's data to display in popup page - */ - popupMax: number - /** - * The default type to display - */ - defaultType: stat.Dimension - /** - * The default duration to search - * @since 0.6.0 - */ - defaultDuration: PopupDuration - /** - * Replace the host name with site name which is detected automatically from the title of site homepages, - * or modified manually by the user - * - * @since 0.5.0 - */ - displaySiteName: boolean - /** - * The start of one week - * - * @since 1.2.5 - */ - weekStart: WeekStartOption - /** - * Whether to merge domain by default - * - * @since 1.3.2 - */ - defaultMergeDomain: boolean - } - - /** - * @since 1.2.5 - */ - type WeekStartOption = - | 'default' - | number // Weekday, From 1 to 7 - - type DarkMode = - // Follow the OS, @since 1.3.3 - | "default" - // Always on - | "on" - // Always off - | "off" - // Timed on - | "timed" - - type AppearanceOption = { - /** - * Whether to display the whitelist button in the context menu - * - * @since 0.3.2 - */ - displayWhitelistMenu: boolean - /** - * Whether to display the badge text of focus time - * - * @since 0.3.3 - */ - displayBadgeText: boolean - /** - * The language of this extension - * - * @since 0.8.0 - */ - locale: LocaleOption - /** - * Whether to print the info in the console - * - * @since 0.8.6 - */ - printInConsole: boolean - /** - * The state of dark mode - * - * @since 1.1.0 - */ - darkMode: DarkMode - /** - * The range of seconds to turn on dark mode. Required if {@param darkMode} is 'timed' - * - * @since 1.1.0 - */ - darkModeTimeStart?: number - darkModeTimeEnd?: number - /** - * The filter of limit mark - * @since 1.3.2 - */ - limitMarkFilter: limit.FilterType - } - - type StatisticsOption = { - /** - * Count when idle - */ - countWhenIdle: boolean - /** - * Whether to collect the site name - * - * @since 0.5.0 - */ - collectSiteName: boolean - /** - * Whether to count the local files - * @since 0.7.0 - */ - countLocalFiles: boolean - } - - /** - * The options of backup - * - * @since 1.2.0 - */ - type BackupOption = { - /** - * The type 2 backup - */ - backupType: backup.Type - /** - * The auth of types, maybe ak/sk or static token - */ - backupAuths: { [type in backup.Type]?: string } - /** - * The name of this client - */ - clientName: string - /** - * Whether to auto-backup data - */ - autoBackUp: boolean - /** - * Interval to auto-backup data, minutes - */ - autoBackUpInterval: number - } - - type AllOption = PopupOption & AppearanceOption & StatisticsOption & BackupOption - /** - * @since 0.8.0 - */ - type LocaleOption = Locale | "default" - } - - namespace meta { - type ExtensionMeta = { - installTime?: number - appCounter?: { [routePath: string]: number } - popupCounter?: { - _total?: number - } - /** - * The id of this client - * - * @since 1.2.0 - */ - cid?: string - backup?: { - [key in timer.backup.Type]?: { - ts: number - msg?: string - } - } - } - } - - /** - * The source locale - * - * @since 1.4.0 - */ - type SourceLocale = 'en' - - /** - * @since 0.8.0 - */ - type Locale = SourceLocale - | 'zh_CN' - | 'ja' - // @since 0.9.0 - | 'zh_TW' - - /** - * Translating locales - * - * @since 1.4.0 - */ - type TranslatingLocale = - | 'de' - | 'es' - | 'ko' - | 'pl' - | 'pt' - | 'pt_BR' - | 'ru' - | 'uk' - | 'fr' - | 'it' - | 'sv' - | 'fi' - | 'da' - | 'hr' - | 'id' - | 'tr' - | 'cs' - | 'ro' - | 'nl' - | 'vi' - | 'sk' - | 'mn' - - namespace stat { - /** - * The dimension to statistics - */ - type Dimension = - // Focus time - | 'focus' - // Visit count - | 'time' - - /** - * Time waste per day - * - * @since 0.0.1 - */ - type Result = { [item in timer.stat.Dimension]: number } - - /** - * Waste data - * - * @since 0.3.3 - */ - type ResultSet = { [host: string]: Result } - - /** - * The unique key of each data row - */ - type RowKey = { - host: string - // Absent if date merged - date?: string - } - - type RowBase = RowKey & Result - - /** - * Row of each statistics result - */ - type Row = RowBase & { - /** - * The merged domains - * - * Can't be empty if merged - * - * @since 0.1.5 - */ - mergedHosts: Row[] - /** - * The composition of data when querying remote - */ - composition?: RemoteComposition - /** - * Icon url - * - * Must be undefined if merged - */ - iconUrl?: string - /** - * The alias name of this Site, always is the title of its homepage by detected - */ - alias?: string - /** - * The id of client where the remote data is storaged - */ - cid?: string - /** - * The name of client where the remote data is storaged - */ - cname?: string - } - - type RemoteCompositionVal = - // Means local data - number | { - /** - * Client's id - */ - cid: string - /** - * Client's name - */ - cname?: string - value: number - } - - /** - * @since 1.4.7 - */ - type RemoteComposition = { - [item in timer.stat.Dimension]: RemoteCompositionVal[] - } - } - - namespace limit { - /** - * Limit rule in runtime - * - * @since 0.8.4 - */ - type Item = Rule & { - regular: RegExp - /** - * Waste today, milliseconds - */ - waste?: number - } - type Rule = { - /** - * Condition, can be regular expression with star signs - */ - cond: string - /** - * Time limit, seconds - */ - time: number - enabled: boolean - /** - * Allow to delay 5 minutes if time over - */ - allowDelay: boolean - } - type Record = Rule & { - /** - * The latest record date - */ - latestDate: string - /** - * Time wasted in the latest record date - */ - wasteTime: number - } - /** - * @since 1.3.2 - */ - type FilterType = - // translucent filter - | 'translucent' - // ground glass filter - | 'groundGlass' - } - - namespace period { - type Key = { - year: number - month: number - date: number - /** - * 0~95 - * ps. 95 = 60 / 15 * 24 - 1 - */ - order: number - } - type Result = Key & { - /** - * 1~900000 - * ps. 900000 = 15min * 60s/min * 1000ms/s - */ - milliseconds: number - } - type Row = { - /** - * {yyyy}{mm}{dd} - */ - date: string - startTime: Date - endTime: Date - /** - * 1 - 60000 - * ps. 60000 = 60s * 1000ms/s - */ - milliseconds: number - } - } - - namespace merge { - type Rule = { - /** - * Origin host, can be regular expression with star signs - */ - origin: string - /** - * The merge result - * - * + Empty string means equals to the origin host - * + Number means the count of kept dots, must be natural number (int & >=0) - */ - merged: string | number - } - interface Merger { - merge(host: string): string - } - } - - namespace common { - type Pagination = { - size: number - num: number - total: number - } - type PageQuery = { - num?: number - size?: number - } - type PageResult = { - list: T[] - total: number - } - } - - namespace app { - /** - * @since 1.1.7 - */ - type TimeFormat = - | "default" - | "second" - | "minute" - | "hour" - } - - /** - * @since 1.2.0 - */ - namespace backup { - - type Type = - | 'none' - | 'gist' - - /** - * Snapshot of last backup - */ - type Snapshot = { - /** - * Timestamp - */ - ts: number - /** - * The date of the ts - */ - date: string - } - - /** - * Snapshot cache - */ - type SnaptshotCache = Partial<{ - [type in Type]: Snapshot - }> - - type MetaCache = Partial> - } - - namespace site { - - /** - * @since 0.5.0 - */ - type AliasSource = - | 'USER' // By user - | 'DETECTED' // Auto-detected - - type AliasKey = { - host: string - /** - * @since 1.2.1 - */ - merged?: boolean - } - /** - * @since 0.5.0 - */ - type AliasValue = { - name: string - source: AliasSource - } - type Alias = AliasKey & AliasValue - type AliasIcon = Alias & { - iconUrl?: string - } - } - - /** - * Message queue - */ - namespace mq { - type ReqCode = - | 'openLimitPage' - | 'limitTimeMeet' - // @since 0.9.0 - | 'limitWaking' - // @since 1.2.3 - | 'limitChanged' - // Request by content script - // @since 1.3.0 - | "cs.isInWhitelist" - | "cs.incVisitCount" - | "cs.printTodayInfo" - | "cs.getTodayInfo" - | "cs.moreMinutes" - | "cs.getLimitedRules" - type ResCode = "success" | "fail" | "ignore" - - /** - * @since 0.2.2 - */ - type Request = { - code: ReqCode - data: T - } - /** - * @since 0.8.4 - */ - type Response = { - code: ResCode, - msg?: string - data?: T - } - /** - * @since 1.3.0 - */ - type Handler = (data: Req) => Promise - /** - * @since 0.8.4 - */ - type Callback = (result?: Response) => void - } -} diff --git a/package.json b/package.json index cd20d9305..ba88d5f03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.5.0", + "version": "1.6.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { diff --git a/script/crowdin/client.ts b/script/crowdin/client.ts index ec354a8a2..73786e2ad 100644 --- a/script/crowdin/client.ts +++ b/script/crowdin/client.ts @@ -9,6 +9,7 @@ import Crowdin, { UploadStorageModel, } from '@crowdin/crowdin-api-client' import axios from 'axios' +import { transMsg } from './common' const PROJECT_ID = 516822 @@ -243,27 +244,18 @@ async function createTranslation(this: CrowdinClient, transKey: TranslationKey, await this.crowdin.stringTranslationsApi.addTranslation(PROJECT_ID, request) } - -const CROWDIN_XML_PATTERN = /(.*?)<\/string>/g - async function downloadTranslations(this: CrowdinClient, fileId: number, lang: CrowdinLanguage): Promise { - const res = await this.crowdin.translationsApi.exportProjectTranslation(PROJECT_ID, { + const res = await this.crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, { targetLanguageId: lang, - fileIds: [fileId], - format: 'android', + skipUntranslatedStrings: true, + exportApprovedOnly: false, + // exportWithMinApprovalsCount: 2, }) const downloadUrl = res?.data?.url const fileRes = await axios.get(downloadUrl) - const xmlData: string = fileRes.data - const items = xmlData.matchAll(CROWDIN_XML_PATTERN) - const itemSet: ItemSet = {} - for (const item of Array.from(items)) { - const result = new RegExp(CROWDIN_XML_PATTERN).exec(item[0]) - const key = result[1] - const text = result[2] - itemSet[key] = text - } - return itemSet + // JSON object + const translation = fileRes.data + return transMsg(translation, '') } /** diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 1f5dc232c..736ce1503 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -71,7 +71,6 @@ export async function readAllMessages(dir: Dir): Promise { - let existMessage: any = existMessages[locale] - if (!existMessage) { - existMessages[locale] = existMessage = {} - } + let existMessage: any = existMessages[locale] || {} Object.entries(itemSet).forEach(([path, text]) => { + if (!text) { + // Not translated + return + } const sourceText = sourceItemSet[path] if (!checkPlaceholder(text, sourceText)) { console.error(`Invalid placeholder: dir=${dir}, filename=${filename}, path=${path}, source=${sourceText}, translated=${text}`) @@ -102,6 +102,10 @@ export async function mergeMessage( const pathSeg = path.split('.') fillItem(pathSeg, 0, existMessage, text) }) + if (Object.keys(existMessage).length) { + // Only merge the locale with any translated strings + existMessages[locale] = existMessage + } }) const existFile = fs.readFileSync(filePath, { encoding: 'utf-8' }) @@ -149,9 +153,9 @@ function generateDefault(existDetault: string, messages: Messages): string return codeLines } -function generateFieldLines(message: Object, indentation: string): string { +function generateFieldLines(messages: Object, indentation: string): string { const lines = [] - Object.entries(message).forEach(([key, value]) => { + Object.entries(messages).forEach(([key, value]) => { let line = undefined if (typeof value === 'object') { const subCodeLines = generateFieldLines(value, indentation + INDENTATION_UNIT) @@ -160,6 +164,8 @@ function generateFieldLines(message: Object, indentation: string): string { const valueText = JSON.stringify(value) // Use double quotes .replace(/'/g, '\\\'').replace(/"/g, '\'') + // Replace tab signs + .replace(/\s{4}/g, '') line = `${indentation}${key}: ${valueText}` } lines.push(line) @@ -201,7 +207,10 @@ export function transMsg(message: any, prefix?: string): ItemSet { Object.entries(subResult) .forEach(([path, val]) => result[path] = val) } else { - result[path] = value + let realVal = value + // Replace tab with blank + typeof value === 'string' && (realVal = value.replace(/\s{4}/g, '')) + result[path] = realVal } }) return result @@ -214,13 +223,3 @@ export async function checkMainBranch(client: CrowdinClient) { } return branch } - -// function main() { -// const file = fs.readFileSync(path.join(MSG_BASE, 'app', 'habit.ts'), { encoding: 'utf-8' }) -// const result = /(const|let|var) _default(.*)=\s*\{\s*(\n?.*\n)+\}/.exec(file) -// const origin = result[0] -// console.log(origin) -// console.log(file.indexOf(origin)) -// } - -// main() diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts index b2b08df91..a44193e4d 100644 --- a/script/crowdin/export-translation.ts +++ b/script/crowdin/export-translation.ts @@ -7,7 +7,7 @@ async function processFile(client: CrowdinClient, file: SourceFilesModel.File, d for (const locale of ALL_TRANS_LOCALES) { const lang = crowdinLangOf(locale) const items: ItemSet = await client.downloadTranslations(file.id, lang) - itemSets[locale] = items + items && Object.keys(items).length && (itemSets[locale] = items) } await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) } diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index f4f8866e9..40de1ae9d 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -54,6 +54,10 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo } for (const locale of ALL_TRANS_LOCALES) { + const translated = message[locale] + if (!translated || !Object.keys(translated).length) { + return + } const strings = transMsg(message[locale]) const crwodinLang = crowdinLangOf(locale) await processDirMessage(client, crowdinFile, strings, crwodinLang) diff --git a/src/api/chrome/action.ts b/src/api/chrome/action.ts new file mode 100644 index 000000000..f0ee2a695 --- /dev/null +++ b/src/api/chrome/action.ts @@ -0,0 +1,11 @@ +import { IS_MV3 } from "@util/constant/environment" +import { handleError } from "./common" + +const action = IS_MV3 ? chrome.action : chrome.browserAction + +export function setBadgeText(text: string, tabId: number): Promise { + return new Promise(resolve => action?.setBadgeText({ tabId, text }, () => { + handleError('setBadgeText') + resolve() + })) +} \ No newline at end of file diff --git a/src/api/chrome/alarm.ts b/src/api/chrome/alarm.ts new file mode 100644 index 000000000..0b4c2c807 --- /dev/null +++ b/src/api/chrome/alarm.ts @@ -0,0 +1,18 @@ +import { handleError } from "./common" + +type AlarmHandler = (alarm: ChromeAlarm) => PromiseLike | void + +export function onAlarm(handler: AlarmHandler) { + chrome.alarms.onAlarm.addListener(handler) +} + +export function clearAlarm(name: string): Promise { + return new Promise(resolve => chrome.alarms.clear(name, () => { + handleError('clearAlarm') + resolve() + })) +} + +export function createAlarm(name: string, when: number): void { + chrome.alarms.create(name, { when }) +} \ No newline at end of file diff --git a/src/api/chrome/common.ts b/src/api/chrome/common.ts new file mode 100644 index 000000000..8e4bd1081 --- /dev/null +++ b/src/api/chrome/common.ts @@ -0,0 +1,8 @@ +export function handleError(scene: string) { + try { + const lastError = chrome.runtime.lastError + lastError && console.log(`Errord when ${scene}: ${lastError.message}`) + } catch (e) { + console.info("Can't execute here") + } +} \ No newline at end of file diff --git a/src/api/chrome/context-menu.ts b/src/api/chrome/context-menu.ts new file mode 100644 index 000000000..e9c5596ea --- /dev/null +++ b/src/api/chrome/context-menu.ts @@ -0,0 +1,15 @@ +import { handleError } from "./common" + +export function createContextMenu(props: ChromeContextMenuCreateProps): Promise { + return new Promise(resolve => chrome.contextMenus.create(props, () => { + handleError('createContextMenu') + resolve() + })) +} + +export function updateContextMenu(menuId: string, props: ChromeContextMenuUpdateProps): Promise { + return new Promise(resolve => chrome.contextMenus.update(menuId, props, () => { + handleError('updateContextMenu') + resolve() + })) +} \ No newline at end of file diff --git a/src/permissions.ts b/src/api/chrome/permissions.ts similarity index 100% rename from src/permissions.ts rename to src/api/chrome/permissions.ts diff --git a/src/api/chrome/runtime.ts b/src/api/chrome/runtime.ts new file mode 100644 index 000000000..918c8a7c0 --- /dev/null +++ b/src/api/chrome/runtime.ts @@ -0,0 +1,41 @@ +import { handleError } from "./common" + +export function getRuntimeId(): string { + return chrome.runtime.id +} + +export function sendMsg2Runtime(code: timer.mq.ReqCode, data?: T): Promise { + const request: timer.mq.Request = { code, data } + return new Promise((resolve, reject) => chrome.runtime.sendMessage(request, + (response: timer.mq.Response) => { + handleError('sendMsg2Runtime') + const resCode = response?.code + resCode === 'success' + ? resolve(response.data) + : reject(new Error(response?.msg)) + }) + ) +} + +export function onRuntimeMessage(handler: ChromeMessageHandler): void { + // Be careful!!! + // Can't use await/async in callback parameter + chrome.runtime.onMessage.addListener((message: timer.mq.Request, sender: chrome.runtime.MessageSender, sendResponse: timer.mq.Callback) => { + handler(message, sender).then((response: timer.mq.Response) => sendResponse(response)) + // 'return ture' will force chrome to wait for the response processed in the above promise. + // @see https://github.com/mozilla/webextension-polyfill/issues/130 + return true + }) +} + +export function onInstalled(handler: (reason: ChromeOnInstalledReason) => void): void { + chrome.runtime.onInstalled.addListener(detail => handler(detail.reason)) +} + +export function getVersion(): string { + return chrome.runtime.getManifest().version +} + +export function setUninstallURL(url: string): Promise { + return new Promise(resolve => chrome.runtime.setUninstallURL(url, resolve)) +} \ No newline at end of file diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts new file mode 100644 index 000000000..7574efded --- /dev/null +++ b/src/api/chrome/tab.ts @@ -0,0 +1,53 @@ +import { handleError } from "./common" + +export function getTab(id: number): Promise { + return new Promise(resolve => chrome.tabs.get(id, tab => { + handleError("getTab") + resolve(tab) + })) +} + +export function createTab(param: chrome.tabs.CreateProperties | string): Promise { + const prop: chrome.tabs.CreateProperties = typeof param === 'string' ? { url: param } : param + return new Promise(resolve => chrome.tabs.create(prop, tab => { + handleError("getTab") + resolve(tab) + })) +} + +export function listTabs(query?: chrome.tabs.QueryInfo): Promise { + query = query || {} + return new Promise(resolve => chrome.tabs.query(query, tabs => { + handleError("listTabs") + resolve(tabs || []) + })) +} + +export function sendMsg2Tab(tabId: number, code: timer.mq.ReqCode, data: T): Promise { + const request: timer.mq.Request = { code, data } + return new Promise((resolve, reject) => { + chrome.tabs.sendMessage, timer.mq.Response>(tabId, request, response => { + handleError('sendMsgTab') + const resCode = response?.code + resCode === 'success' + ? resolve(response.data) + : reject(new Error(response?.msg)) + }) + }) +} + +type TabHandler = (tabId: number, ev: Event, tab?: ChromeTab) => void + +export function onTabActivated(handler: TabHandler): void { + chrome.tabs.onActivated.addListener((activeInfo: chrome.tabs.TabActiveInfo) => { + handleError("tabActivated") + handler(activeInfo?.tabId, activeInfo) + }) +} + +export function onTabUpdated(handler: TabHandler): void { + chrome.tabs.onUpdated.addListener((tabId: number, changeInfo: ChromeTabChangeInfo, tab: ChromeTab) => { + handleError("tabUpdated") + handler(tabId, changeInfo, tab) + }) +} \ No newline at end of file diff --git a/src/api/chrome/window.ts b/src/api/chrome/window.ts new file mode 100644 index 000000000..b40fc63da --- /dev/null +++ b/src/api/chrome/window.ts @@ -0,0 +1,45 @@ +import { handleError } from "./common" + + +export function listAllWindows(): Promise { + return new Promise(resolve => chrome.windows.getAll(windows => { + handleError("listAllWindows") + resolve(windows || []) + })) +} + +export function isNoneWindowId(windowId: number) { + return !windowId || windowId === chrome.windows.WINDOW_ID_NONE +} + +export function getFocusedNormalWindow(): Promise { + return new Promise(resolve => chrome.windows.getLastFocused( + // Only find normal window + { windowTypes: ['normal'] }, + window => { + handleError('getFocusedNormalWindow') + if (!window?.focused || isNoneWindowId(window?.id)) { + resolve(undefined) + } else { + resolve(window) + } + } + )) +} + +export function getWindow(id: number): Promise { + return new Promise(resolve => + chrome.windows.get(id) + .then(win => resolve(win)) + .catch(_ => resolve(undefined)) + ) +} + +type _Handler = (windowId: number) => void + +export function onNormalWindowFocusChanged(handler: _Handler) { + chrome.windows.onFocusChanged.addListener(windowId => { + handleError('onWindowFocusChanged') + handler(windowId) + }, { windowTypes: ['normal'] }) +} \ No newline at end of file diff --git a/src/app/components/report/table/columns/alias-info.ts b/src/app/components/common/editable.ts similarity index 97% rename from src/app/components/report/table/columns/alias-info.ts rename to src/app/components/common/editable.ts index c54f7639a..ac7c55fdf 100644 --- a/src/app/components/report/table/columns/alias-info.ts +++ b/src/app/components/common/editable.ts @@ -85,14 +85,14 @@ function render(data: _Data, ctx: SetupContext<_Emits>) { * @since 0.7.1 */ const _default = defineComponent({ - name: "ReportAliasInfo", + name: "Editable", props: { modelValue: { type: String } }, emits: { - change: (_newAlias: string) => true + change: (_newVal: string) => true }, setup(props, ctx) { const editing = ref(false) diff --git a/src/app/components/common/host-alert.ts b/src/app/components/common/host-alert.ts index 14b43e428..645ad4c08 100644 --- a/src/app/components/common/host-alert.ts +++ b/src/app/components/common/host-alert.ts @@ -47,20 +47,21 @@ const _default = defineComponent({ underline: props.clickable, style: { cursor: cursor.value } }, () => props.host) - : () => h('div', [ - h(ElLink, - { + : () => { + const children = [ + h(ElLink, { href: href.value, target: target.value, underline: props.clickable, style: { cursor: cursor.value } - }, - () => props.host - ), h('span', + }, () => props.host) + ] + props.iconUrl && children.push(h('span', { style: HOST_ICON_STYLE }, h('img', { src: props.iconUrl, width: 12, height: 12 }) - ) - ]) + )) + return h('div', children) + } } }) diff --git a/src/app/components/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 1bdc5e277..8d664cecc 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -28,7 +28,7 @@ use([ ]) import { t } from "@app/locale" -import timerService, { TimerQueryParam } from "@service/timer-service" +import statService, { StatQueryParam } from "@service/stat-service" import { locale } from "@i18n" import { formatTime, getWeeksAgo, MILL_PER_DAY, MILL_PER_MINUTE } from "@util/time" import { ElLoading } from "element-plus" @@ -38,6 +38,7 @@ import { BASE_TITLE_OPTION } from "../common" import { getPrimaryTextColor } from "@util/style" import { getAppPageUrl } from "@util/constant/url" import { REPORT_ROUTE } from "@app/router/constants" +import { createTab } from "@api/chrome/tab" const WEEK_NUM = 53 @@ -213,7 +214,7 @@ function handleClick(value: _Value): void { const query: ReportQueryParam = { ds: currentTs, de: currentTs } const url = getAppPageUrl(false, REPORT_ROUTE, query) - chrome.tabs.create({ url }) + createTab(url) } class ChartWrapper { @@ -268,8 +269,8 @@ const _default = defineComponent({ // 2. init chart chartWrapper.init(chart.value) // 3. query data - const query: TimerQueryParam = { date: [startTime, now], sort: "date" } - const items = await timerService.select(query) + const query: StatQueryParam = { date: [startTime, now], sort: "date" } + const items = await statService.select(query) const result = {} items.forEach(({ date, focus }) => result[date] = (result[date] || 0) + focus) // 4. set weekdays diff --git a/src/app/components/dashboard/components/indicator/index.ts b/src/app/components/dashboard/components/indicator/index.ts index fc031f92a..1cf3b0097 100644 --- a/src/app/components/dashboard/components/indicator/index.ts +++ b/src/app/components/dashboard/components/indicator/index.ts @@ -6,7 +6,7 @@ */ import PeriodDatabase from "@db/period-database" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import { getStartOfDay, MILL_PER_DAY, MILL_PER_MINUTE } from "@util/time" import { defineComponent, h, ref, Ref } from "vue" import { groupBy } from "@util/array" @@ -36,7 +36,7 @@ function calculateInstallDays(installTime: Date, now: Date): number { } async function query(): Promise<_Value> { - const allData: timer.stat.Row[] = await timerService.select() + const allData: timer.stat.Row[] = await statService.select() const hostSet = new Set() let visits = 0 let browsingTime = 0 diff --git a/src/app/components/dashboard/components/top-k-visit.ts b/src/app/components/dashboard/components/top-k-visit.ts index c8dffdb76..2c504f5a4 100644 --- a/src/app/components/dashboard/components/top-k-visit.ts +++ b/src/app/components/dashboard/components/top-k-visit.ts @@ -9,7 +9,7 @@ import type { ECharts, ComposeOption } from "echarts/core" import type { PieSeriesOption } from "echarts/charts" import type { TitleComponentOption, TooltipComponentOption } from "echarts/components" import type { Ref } from "vue" -import type { TimerQueryParam } from "@service/timer-service" +import type { StatQueryParam } from "@service/stat-service" import { init, use } from "@echarts/core" import PieChart from "@echarts/chart/pie" @@ -18,7 +18,7 @@ import TooltipComponent from "@echarts/component/tooltip" use([PieChart, TitleComponent, TooltipComponent]) -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import { MILL_PER_DAY } from "@util/time" import { ElLoading } from "element-plus" import { defineComponent, h, onMounted, ref } from "vue" @@ -110,13 +110,13 @@ const _default = defineComponent({ target: `#${CONTAINER_ID}`, }) chartWrapper.init(chart.value) - const query: TimerQueryParam = { + const query: StatQueryParam = { date: [startTime, now], sort: "time", sortOrder: 'DESC', mergeDate: true, } - const top: timer.stat.Row[] = (await timerService.selectByPage(query, { num: 1, size: TOP_NUM }, { alias: true })).list + const top: timer.stat.Row[] = (await statService.selectByPage(query, { num: 1, size: TOP_NUM }, true)).list const data: _Value[] = top.map(({ time, host, alias }) => ({ name: alias || host, host, alias, value: time })) for (let realSize = top.length; realSize < TOP_NUM; realSize++) { data.push({ name: '', host: '', value: 0 }) diff --git a/src/app/components/dashboard/components/week-on-week.ts b/src/app/components/dashboard/components/week-on-week.ts index 0618a518c..9ba2a3516 100644 --- a/src/app/components/dashboard/components/week-on-week.ts +++ b/src/app/components/dashboard/components/week-on-week.ts @@ -6,7 +6,7 @@ */ import type { Ref } from "vue" -import type { FillFlagParam, TimerQueryParam } from "@service/timer-service" +import type { StatQueryParam } from "@service/stat-service" import type { ECharts, ComposeOption } from "echarts/core" import type { CandlestickSeriesOption } from "echarts/charts" import type { GridComponentOption, TitleComponentOption, TooltipComponentOption } from "echarts/components" @@ -22,7 +22,7 @@ use([CandlestickChart, GridComponent, TitleComponent, TooltipComponent]) import { formatPeriodCommon, MILL_PER_DAY } from "@util/time" import { ElLoading } from "element-plus" import { defineComponent, h, onMounted, ref } from "vue" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import { groupBy, sum } from "@util/array" import { BASE_TITLE_OPTION } from "../common" import { t } from "@app/locale" @@ -49,6 +49,17 @@ type _Value = { host: string } +const X_AXIS_LABEL_MAX_LENGTH = 16 + +function calculateXAixsLabel(host: string, hostAliasMap: Record) { + const originLabel = hostAliasMap[host] || host + const originLength = originLabel?.length + if (!originLength || originLength <= X_AXIS_LABEL_MAX_LENGTH) { + return originLabel + } + return originLabel.substring(0, X_AXIS_LABEL_MAX_LENGTH - 3) + '...' +} + function optionOf(lastPeriodItems: timer.stat.Row[], thisPeriodItems: timer.stat.Row[]): EcOption { const textColor = getPrimaryTextColor() @@ -133,7 +144,7 @@ function optionOf(lastPeriodItems: timer.stat.Row[], thisPeriodItems: timer.stat axisLabel: { interval: 0, color: textColor, - formatter: (host: string) => hostAliasMap[host] || host + formatter: (host: string) => calculateXAixsLabel(host, hostAliasMap) }, }, yAxis: { @@ -186,16 +197,15 @@ const _default = defineComponent({ target: `#${CONTAINER_ID}`, }) chartWrapper.init(chart.value) - const query: TimerQueryParam = { + const query: StatQueryParam = { date: [lastPeriodStart, lastPeriodEnd], mergeDate: true, } // Query with alias // @since 1.1.8 - const flagParam: FillFlagParam = { alias: true } - const lastPeriodItems: timer.stat.Row[] = await timerService.select(query, flagParam) + const lastPeriodItems: timer.stat.Row[] = await statService.select(query, true) query.date = [thisPeriodStart, thisPeriodEnd] - const thisPeriodItems: timer.stat.Row[] = await timerService.select(query, flagParam) + const thisPeriodItems: timer.stat.Row[] = await statService.select(query, true) const option = optionOf(lastPeriodItems, thisPeriodItems) chartWrapper.render(option, loading) }) diff --git a/src/app/components/data-manage/clear/filter/operation-button.ts b/src/app/components/data-manage/clear/filter/operation-button.ts index 9027a225e..d96b1bf24 100644 --- a/src/app/components/data-manage/clear/filter/operation-button.ts +++ b/src/app/components/data-manage/clear/filter/operation-button.ts @@ -8,14 +8,14 @@ import { ElButton, ElMessage, ElMessageBox, ElTooltip } from "element-plus" import ElementIcon from "@src/element-ui/icon" import { Ref, h } from "vue" -import TimerDatabase, { TimerCondition } from "@db/timer-database" +import StatDatabase, { StatCondition } from "@db/stat-database" import { ItemMessage } from "@i18n/message/common/item" import { t } from "@src/app/locale" import { DataManageMessage } from "@i18n/message/app/data-manage" import { MILL_PER_DAY } from "@util/time" import { ElementButtonType } from "@src/element-ui/button" -const timerDatabase = new TimerDatabase(chrome.storage.local) +const statDatabase = new StatDatabase(chrome.storage.local) export type BaseFilterProps = { focusStartRef: Ref @@ -77,7 +77,7 @@ const str2Range = (startAndEnd: Ref[], numAmplifier?: (origin: number) = const seconds2Milliseconds = (a: number) => a * 1000 -function checkParam(props: _Props): TimerCondition | undefined { +function checkParam(props: _Props): StatCondition | undefined { const { focusStartRef, focusEndRef, timeStartRef, timeEndRef } = props let hasError = false const focusRange = str2Range([focusStartRef, focusEndRef], seconds2Milliseconds) @@ -87,7 +87,7 @@ function checkParam(props: _Props): TimerCondition | undefined { if (hasError) { return undefined } - const condition: TimerCondition = {} + const condition: StatCondition = {} condition.focusRange = focusRange condition.timeRange = timeRange return condition @@ -108,7 +108,7 @@ function generateParamAndSelect(props: _Props): Promise | unde } condition.date = [dateStart, dateEnd] - return timerDatabase.select(condition) + return statDatabase.select(condition) } const operationCancelMsg = t(msg => msg.dataManage.operationCancel) diff --git a/src/app/components/data-manage/clear/index.ts b/src/app/components/data-manage/clear/index.ts index ef188ca28..40720fdb1 100644 --- a/src/app/components/data-manage/clear/index.ts +++ b/src/app/components/data-manage/clear/index.ts @@ -10,14 +10,14 @@ import { defineComponent, h, Ref, ref, SetupContext } from "vue" import { t } from "@app/locale" import { alertProps } from "../common" import Filter from "./filter" -import TimerDatabase, { TimerCondition } from "@db/timer-database" +import StatDatabase, { StatCondition } from "@db/stat-database" import { MILL_PER_DAY } from "@util/time" type _Emits = { dataDelete: () => true } -const timerDatabase = new TimerDatabase(chrome.storage.local) +const statDatabase = new StatDatabase(chrome.storage.local) const operationCancelMsg = t(msg => msg.dataManage.operationCancel) const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) @@ -32,7 +32,7 @@ async function handleClick(filterRef: Ref, ctx: SetupContext<_Emits>) { cancelButtonText: operationCancelMsg, confirmButtonText: operationConfirmMsg }).then(async () => { - await timerDatabase.delete(result) + await statDatabase.delete(result) ElMessage(t(msg => msg.dataManage.deleteSuccess)) ctx.emit('dataDelete') }).catch(() => { }) @@ -53,7 +53,7 @@ function generateParamAndSelect(props: DataManageClearFilterOption): Promise (str && str !== '') ? parseInt(str) : defaultVal const seconds2Milliseconds = (a: number) => a * 1000 -function checkParam(filterOption: DataManageClearFilterOption): TimerCondition | undefined { +function checkParam(filterOption: DataManageClearFilterOption): StatCondition | undefined { const { focusStart, focusEnd, timeStart, timeEnd } = filterOption let hasError = false const focusRange = str2Range([focusStart, focusEnd], seconds2Milliseconds) @@ -87,7 +87,7 @@ function checkParam(filterOption: DataManageClearFilterOption): TimerCondition | if (hasError) { return undefined } - const condition: TimerCondition = {} + const condition: StatCondition = {} condition.focusRange = focusRange condition.timeRange = timeRange return condition diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index ac1637355..a84dc1cbf 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -62,7 +62,7 @@ function convert2Info(translationStatus: TranslationStatusInfo): ProgressInfo { function computeType(progress: number): 'success' | '' | 'warning' { if (progress >= 95) { return "success" - } else if (progress >= 80) { + } else if (progress >= 50) { return "" } else { return "warning" diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index b595c63d6..9522e3c9e 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -9,7 +9,7 @@ import { defineComponent, h, PropType, ref, Ref, VNode, watch } from "vue" import clipboardy from "clipboardy" import { t } from "@app/locale" import { ElButton, ElFormItem, ElInput, ElOption, ElSelect } from "element-plus" -import { checkPermission, requestPermission } from "@src/permissions" +import { checkPermission, requestPermission } from "@api/chrome/permissions" import { IS_FIREFOX } from "@util/constant/environment" import { parseUrl } from "./common" diff --git a/src/app/components/option/common.ts b/src/app/components/option/common.ts index 4d74535ac..c8a323873 100644 --- a/src/app/components/option/common.ts +++ b/src/app/components/option/common.ts @@ -18,7 +18,11 @@ import { OptionMessage } from "@i18n/message/app/option" * @param label label * @param defaultValue default value */ -export function renderOptionItem(input: VNode | { [key: string]: VNode }, label: (msg: OptionMessage) => string, defaultValue?: string | number) { +export function renderOptionItem( + input: VNode | { [key: string]: VNode }, + label: (msg: OptionMessage | EmbeddedPartial) => string, + defaultValue?: string | number +) { const param = isVNode(input) ? { input } : input const labelArcher = h('a', { class: 'option-label' }, tN(msg => label(msg.option), param)) const content = [labelArcher] diff --git a/src/app/components/report/filter/index.ts b/src/app/components/report/filter/index.ts index 084e8b985..865f0f41b 100644 --- a/src/app/components/report/filter/index.ts +++ b/src/app/components/report/filter/index.ts @@ -20,7 +20,7 @@ import DateRangeFilterItem from "@app/components/common/date-range-filter-item" import { daysAgo } from "@util/time" import { ElButton } from "element-plus" import { DeleteFilled } from "@element-plus/icons-vue" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" const hostPlaceholder = t(msg => msg.report.hostPlaceholder) const mergeDateLabel = t(msg => msg.report.mergeDate) @@ -84,7 +84,7 @@ const _default = defineComponent({ timeFormat: timeFormat.value } as ReportFilterOption) const handleChange = () => ctx.emit("change", computeOption()) - timerService.canReadRemote().then(abled => remoteSwitchVisible.value = abled) + statService.canReadRemote().then(abled => remoteSwitchVisible.value = abled) return () => [ h(InputFilterItem, { placeholder: hostPlaceholder, diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index 308b4eb55..25393c465 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -6,12 +6,12 @@ */ import type { Ref, UnwrapRef, ComputedRef } from "vue" -import type { TimerQueryParam } from "@service/timer-service" +import type { StatQueryParam } from "@service/stat-service" import type { Router, RouteLocation } from "vue-router" import { computed, defineComponent, h, reactive, ref } from "vue" import { I18nKey, t } from "@app/locale" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import whitelistService from "@service/whitelist-service" import './styles/element' import ReportTable from "./table" @@ -19,43 +19,41 @@ import ReportFilter from "./filter" import Pagination from "../common/pagination" import ContentContainer from "../common/content-container" import { ElLoadingService, ElMessage, ElMessageBox } from "element-plus" -import hostAliasService from "@service/host-alias-service" +import siteService from "@service/site-service" import { exportCsv, exportJson } from "./file-export" import { useRoute, useRouter } from "vue-router" import { groupBy, sum } from "@util/array" import { formatTime } from "@util/time" -import TimerDatabase from "@db/timer-database" -import { IS_SAFARI } from "@util/constant/environment" +import StatDatabase from "@db/stat-database" import { handleWindowVisibleChange } from "@util/window" -const timerDatabase = new TimerDatabase(chrome.storage.local) +const statDatabase = new StatDatabase(chrome.storage.local) async function queryData( - queryParam: Ref, + queryParam: Ref, data: Ref, page: UnwrapRef, readRemote: Ref ) { const loading = ElLoadingService({ target: `.container-card>.el-card__body`, text: "LOADING..." }) const pageInfo = { size: page.size, num: page.num } - const fillFlag = { alias: true, iconUrl: !IS_SAFARI } const param = { ...queryParam.value, inclusiveRemote: readRemote.value } - const pageResult = await timerService.selectByPage(param, pageInfo, fillFlag) + const pageResult = await statService.selectByPage(param, pageInfo, true) const { list, total } = pageResult data.value = list page.total = total loading.close() } -async function handleAliasChange(key: timer.site.AliasKey, newAlias: string, data: Ref) { +async function handleAliasChange(key: timer.site.SiteKey, newAlias: string, data: Ref) { newAlias = newAlias?.trim?.() if (!newAlias) { - await hostAliasService.remove(key) + await siteService.removeAlias(key) } else { - await hostAliasService.change(key, newAlias) + await siteService.saveAlias(key, newAlias, 'USER') } data.value .filter(item => item.host === key.host) @@ -80,7 +78,7 @@ async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: bool } const count2Delete: number = mergeDate // All the items - ? sum(await Promise.all(Array.from(hosts).map(host => timerService.count({ host, fullHost: true, date: dateRange })))) + ? sum(await Promise.all(Array.from(hosts).map(host => statService.count({ host, fullHost: true, date: dateRange })))) // The count of row : selected?.length || 0 const i18nParam = { @@ -154,12 +152,12 @@ async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateR if (!mergeDate) { // If not merge date // Delete batch - await timerDatabase.delete(selected) + await statDatabase.delete(selected) } else { // Delete according to the date range const start = dateRange?.[0] const end = dateRange?.[1] - await Promise.all(selected.map(d => timerDatabase.deleteByUrlBetween(d.host, start, end))) + await Promise.all(selected.map(d => statDatabase.deleteByUrlBetween(d.host, start, end))) } } @@ -189,7 +187,7 @@ function initQueryParam(route: RouteLocation, router: Router): [ReportFilterOpti return [filterOption, sortInfo] } -function computeTimerQueryParam(filterOption: ReportFilterOption, sort: SortInfo): TimerQueryParam { +function computeTimerQueryParam(filterOption: ReportFilterOption, sort: SortInfo): StatQueryParam { return { host: filterOption.host, date: filterOption.dateRange, @@ -222,7 +220,7 @@ const _default = defineComponent({ const remoteRead: Ref = ref(false) const page: UnwrapRef = reactive({ size: 10, num: 1, total: 0 }) - const queryParam: ComputedRef = computed(() => computeTimerQueryParam(filterOption, sort)) + const queryParam: ComputedRef = computed(() => computeTimerQueryParam(filterOption, sort)) const tableEl: Ref = ref() const query = () => queryData(queryParam, data, page, remoteRead) @@ -243,7 +241,7 @@ const _default = defineComponent({ query() }, onDownload: async (format: FileFormat) => { - const rows = await timerService.select(queryParam.value, { alias: true }) + const rows = await statService.select(queryParam.value, true) format === 'json' && exportJson(filterOption, rows) format === 'csv' && exportCsv(filterOption, rows) }, diff --git a/src/app/components/report/table/columns/alias.ts b/src/app/components/report/table/columns/alias.ts index a7718f1ba..ba14265eb 100644 --- a/src/app/components/report/table/columns/alias.ts +++ b/src/app/components/report/table/columns/alias.ts @@ -11,7 +11,7 @@ import { ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" -import ReportAliasInfo from "./alias-info" +import Editable from "@app/components/common/editable" const columnLabel = t(msg => msg.siteManage.column.alias) @@ -26,7 +26,7 @@ const _default = defineComponent({ minWidth: 140, align: "center" }, { - default: ({ row }: { row: timer.stat.Row }) => h(ReportAliasInfo, { + default: ({ row }: { row: timer.stat.Row }) => h(Editable, { modelValue: row.alias, onChange: (newAlias: string) => ctx.emit("aliasChange", row.host, newAlias) }) diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index 526a7c5d7..42cf1a773 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -12,7 +12,7 @@ import type { PropType } from "vue" import { computed, defineComponent, h } from "vue" import { ElButton, ElMessage, ElTableColumn } from "element-plus" -import TimerDatabase from "@db/timer-database" +import StatDatabase from "@db/stat-database" import whitelistService from "@service/whitelist-service" import { t } from "@app/locale" import { LocationQueryRaw, Router, useRouter } from "vue-router" @@ -22,17 +22,17 @@ import OperationPopupConfirmButton from "@app/components/common/popup-confirm-bu import OperationDeleteButton from "./operation-delete-button" import { locale } from "@i18n" -const timerDatabase = new TimerDatabase(chrome.storage.local) +const statDatabase = new StatDatabase(chrome.storage.local) async function handleDeleteByRange(itemHost2Delete: string, dateRange: Array): Promise { // Delete all if (!dateRange || !dateRange.length) { - return await timerDatabase.deleteByUrl(itemHost2Delete) + return await statDatabase.deleteByUrl(itemHost2Delete) } // Delete by range const start = dateRange[0] const end = dateRange[1] - await timerDatabase.deleteByUrlBetween(itemHost2Delete, start, end) + await statDatabase.deleteByUrlBetween(itemHost2Delete, start, end) } const columnLabel = t(msg => msg.item.operation.label) @@ -89,7 +89,7 @@ const _default = defineComponent({ const host = row.host props.mergeDate ? await handleDeleteByRange(host, props.dateRange) - : await timerDatabase.deleteByUrlAndDate(host, row.date) + : await statDatabase.deleteByUrlAndDate(host, row.date) ctx.emit("delete", row) } }), diff --git a/src/app/components/site-manage/common.ts b/src/app/components/site-manage/common.ts index ee137db5b..307ec5732 100644 --- a/src/app/components/site-manage/common.ts +++ b/src/app/components/site-manage/common.ts @@ -7,25 +7,40 @@ import { t } from "@app/locale" +const MERGED_FLAG = 'm' +const VIRTUAL_FLAG = 'v' +const NONE_FLAG = '_' + /** - * key => value + * site key => option value */ -export function optionValueOf(aliasKey: timer.site.AliasKey): string { - if (!aliasKey) return '' - return `${aliasKey.merged ? 'm' : '_'}${aliasKey.host}` +export function cvt2OptionValue(siteKey: timer.site.SiteKey): string { + if (!siteKey) return '' + const { merged, virtual } = siteKey + let flag = NONE_FLAG + merged && (flag = MERGED_FLAG) + virtual && (flag = VIRTUAL_FLAG) + return `${flag}${siteKey.host}` } /** - * value => key + * option value => site key */ -export function aliasKeyOf(value: string): timer.site.AliasKey { - const merged = value.startsWith('m') - const host = value.substring(1) - return { host, merged } +export function cvt2SiteKey(optionValue: string): timer.site.SiteKey { + const flag = optionValue.substring(0, 1) + const host = optionValue.substring(1) + if (flag === MERGED_FLAG) { + return { host, merged: true } + } else if (flag === VIRTUAL_FLAG) { + return { host, virtual: true } + } else { + return { host } + } } export const EXIST_MSG = t(msg => msg.siteManage.msg.existedTag) export const MERGED_MSG = t(msg => msg.siteManage.msg.mergedTag) +export const VIRTUAL_MSG = t(msg => msg.siteManage.msg.virtualTag) /** * Calclate the label of alias key to display @@ -34,12 +49,15 @@ export const MERGED_MSG = t(msg => msg.siteManage.msg.mergedTag) * 1. www.google.com * 2. www.google.com[MERGED] * 4. www.google.com[EXISTED] + * 5. www.github.com/sheepzh/*[VIRTUAL] + * 5. www.github.com/sheepzh/*[VIRTUAL-EXISTED] * 3. www.google.com[MERGED-EXISTED] */ -export function labelOf(aliasKey: timer.site.AliasKey, exists?: boolean): string { - let label = aliasKey.host +export function labelOf(siteKey: timer.site.SiteKey, exists?: boolean): string { + let label = siteKey.host const suffix = [] - aliasKey.merged && suffix.push(MERGED_MSG) + siteKey.merged && suffix.push(MERGED_MSG) + siteKey.virtual && suffix.push(VIRTUAL_MSG) exists && suffix.push(EXIST_MSG) suffix.length && (label += `[${suffix.join('-')}]`) return label diff --git a/src/app/components/site-manage/index.ts b/src/app/components/site-manage/index.ts index f59e55e5d..9d4a929f8 100644 --- a/src/app/components/site-manage/index.ts +++ b/src/app/components/site-manage/index.ts @@ -12,7 +12,7 @@ import ContentContainer from "../common/content-container" import SiteManageFilter from "./filter" import Pagination from "../common/pagination" import SiteManageTable from "./table" -import hostAliasService, { HostAliasQueryParam } from "@service/host-alias-service" +import siteService, { SiteQueryParam } from "@service/site-service" import Modify from './modify' export default defineComponent({ @@ -25,7 +25,7 @@ export default defineComponent({ get: () => sourceRef.value == 'DETECTED', set: (val: boolean) => sourceRef.value = val ? 'DETECTED' : undefined }) - const dataRef: Ref = ref([]) + const dataRef: Ref = ref([]) const modifyDialogRef: Ref = ref() const pageRef: UnwrapRef = reactive({ @@ -34,7 +34,7 @@ export default defineComponent({ total: 0 }) - const queryParam: ComputedRef = computed(() => ({ + const queryParam: ComputedRef = computed(() => ({ host: hostRef.value, alias: aliasRef.value, source: sourceRef.value @@ -43,7 +43,7 @@ export default defineComponent({ async function queryData() { const page = { size: pageRef.size, num: pageRef.num } - const pageResult = await hostAliasService.selectByPage(queryParam.value, page) + const pageResult = await siteService.selectByPage(queryParam.value, page) const { list, total } = pageResult dataRef.value = list pageRef.total = total @@ -69,11 +69,7 @@ export default defineComponent({ content: () => [ h(SiteManageTable, { data: dataRef.value, - onRowModify: async (row: timer.site.AliasIcon) => modifyDialogRef.value.modify(row), - onRowDelete: async (row: timer.site.AliasIcon) => { - await hostAliasService.remove(row) - queryData() - } + onRowDelete: queryData }), h(Pagination, { size: pageRef.size, diff --git a/src/app/components/site-manage/modify/name-form-item.ts b/src/app/components/site-manage/modify/alias-form-item.ts similarity index 91% rename from src/app/components/site-manage/modify/name-form-item.ts rename to src/app/components/site-manage/modify/alias-form-item.ts index 92e4ec8ed..58183ff2b 100644 --- a/src/app/components/site-manage/modify/name-form-item.ts +++ b/src/app/components/site-manage/modify/alias-form-item.ts @@ -11,7 +11,7 @@ import { h, defineComponent } from "vue" const LABEL = t(msg => msg.siteManage.column.alias) const _default = defineComponent({ - name: "SiteManageNameFormItem", + name: "SiteManageAliasFormItem", props: { modelValue: String }, @@ -21,7 +21,7 @@ const _default = defineComponent({ }, setup(props, ctx) { return () => h(ElFormItem, - { prop: 'name', label: LABEL }, + { prop: 'alias', label: LABEL }, () => h(ElInput, { modelValue: props.modelValue, onInput: (newVal: string) => ctx.emit("input", newVal.trimStart()), diff --git a/src/app/components/site-manage/modify/host-form-item.ts b/src/app/components/site-manage/modify/host-form-item.ts index 20808923f..67341049b 100644 --- a/src/app/components/site-manage/modify/host-form-item.ts +++ b/src/app/components/site-manage/modify/host-form-item.ts @@ -8,17 +8,16 @@ import type { PropType, Ref, VNode } from "vue" import { t } from "@app/locale" -import HostAliasDatabase from "@db/host-alias-database" -import timerService, { HostSet } from "@service/timer-service" -import { ElFormItem, ElInput, ElOption, ElSelect, ElTag } from "element-plus" +import statService, { HostSet } from "@service/stat-service" +import { ElFormItem, ElOption, ElSelect, ElTag } from "element-plus" import { defineComponent, h, ref } from "vue" -import { aliasKeyOf, EXIST_MSG, labelOf, MERGED_MSG, optionValueOf } from "../common" +import { cvt2SiteKey, cvt2OptionValue, EXIST_MSG, MERGED_MSG, VIRTUAL_MSG, labelOf } from "../common" import { ALL_HOSTS, MERGED_HOST } from "@util/constant/remain-host" - -const hostAliasDatabase = new HostAliasDatabase(chrome.storage.local) +import siteService from "@service/site-service" +import { isValidVirtualHost } from "@util/pattern" type _OptionInfo = { - aliasKey: timer.site.AliasKey + siteKey: timer.site.SiteKey hasAlias: boolean } @@ -27,8 +26,8 @@ async function handleRemoteSearch(query: string, searching: Ref, search return } searching.value = true - const hostSet: HostSet = (await timerService.listHosts(query)) - const allAlias: timer.site.AliasKey[] = + const hostSet: HostSet = (await statService.listHosts(query)) + const allAlias: timer.site.SiteKey[] = [ ...Array.from(hostSet.origin || []).map(host => ({ host, merged: false })), ...Array.from(hostSet.merged || []).map(host => ({ host, merged: true })), @@ -37,66 +36,69 @@ async function handleRemoteSearch(query: string, searching: Ref, search ALL_HOSTS.forEach(remain => allAlias.push({ host: remain, merged: false })) allAlias.push({ host: MERGED_HOST, merged: true }) const existedAliasSet = new Set() - const existedKeys: timer.site.AliasKey[] = (await hostAliasDatabase.existBatch(allAlias)) - existedKeys.forEach(key => existedAliasSet.add(optionValueOf(key))) + const existedKeys = await siteService.existBatch(allAlias) + existedKeys.forEach(key => existedAliasSet.add(cvt2OptionValue(key))) const existedOptions = [] const notExistedOptions = [] - allAlias.forEach(aliasKey => { - const hasAlias = existedAliasSet.has(optionValueOf(aliasKey)) - const props: _OptionInfo = { aliasKey, hasAlias } + allAlias.forEach(siteKey => { + const hasAlias = existedAliasSet.has(cvt2OptionValue(siteKey)) + const props: _OptionInfo = { siteKey, hasAlias } hasAlias ? existedOptions.push(props) : notExistedOptions.push(props) }) - // Not exist first - searchedHosts.value = [...notExistedOptions, ...existedOptions] + + const originalOptions = [...notExistedOptions, ...existedOptions] + + const result = [] + // Not exist host, insert virtual site into the first + const existsHost = originalOptions.find(o => o.siteKey?.host === query) + !existsHost && isValidVirtualHost(query) && result.push({ siteKey: { host: query, virtual: true }, hasAlias: false }) + result.push(...originalOptions) + + searchedHosts.value = result searching.value = false } -function renderOptionSlots(aliasKey: timer.site.AliasKey, hasAlias: boolean): VNode[] { - const { host, merged } = aliasKey +function renderOptionSlots(siteKey: timer.site.SiteKey, hasAlias: boolean): VNode[] { + const { host, merged, virtual } = siteKey const result = [ h('span', {}, host) ] - merged && result.push(h(ElTag, { size: 'small' }, MERGED_MSG)) - hasAlias && result.push(h(ElTag, { size: 'small', type: 'info' }, EXIST_MSG)) + merged && result.push(h(ElTag, { size: 'small' }, () => MERGED_MSG)) + virtual && result.push(h(ElTag, { size: 'small' }, () => VIRTUAL_MSG)) + hasAlias && result.push(h(ElTag, { size: 'small', type: 'info' }, () => EXIST_MSG)) return result } -function renderOption({ aliasKey, hasAlias }: _OptionInfo) { +function renderOption({ siteKey, hasAlias }: _OptionInfo) { return h(ElOption, { - value: optionValueOf(aliasKey), + value: cvt2OptionValue(siteKey), disabled: hasAlias, - label: labelOf(aliasKey, hasAlias) - }, () => renderOptionSlots(aliasKey, hasAlias)) + label: labelOf(siteKey, hasAlias) + }, () => renderOptionSlots(siteKey, hasAlias)) } const HOST_LABEL = t(msg => msg.siteManage.column.host) const _default = defineComponent({ name: "SiteManageHostFormItem", props: { - editing: Boolean, - modelValue: Object as PropType + modelValue: Object as PropType }, emits: { - change: (_aliasKey: timer.site.AliasKey) => true + change: (_siteKey: timer.site.SiteKey) => true }, setup(props, ctx) { const searching: Ref = ref(false) const searchedHosts: Ref<_OptionInfo[]> = ref([]) return () => h(ElFormItem, { prop: 'key', label: HOST_LABEL }, - () => props.editing ? - h(ElSelect, { - style: { width: '100%' }, - modelValue: optionValueOf(props.modelValue), - filterable: true, - remote: true, - loading: searching.value, - remoteMethod: (query: string) => handleRemoteSearch(query, searching, searchedHosts), - onChange: (newVal: string) => ctx.emit("change", newVal ? aliasKeyOf(newVal) : undefined) - }, () => searchedHosts.value?.map(renderOption)) - : h(ElInput, { - disabled: true, - modelValue: labelOf(props.modelValue) - }) + () => h(ElSelect, { + style: { width: '100%' }, + modelValue: cvt2OptionValue(props.modelValue), + filterable: true, + remote: true, + loading: searching.value, + remoteMethod: (query: string) => handleRemoteSearch(query, searching, searchedHosts), + onChange: (newVal: string) => ctx.emit("change", newVal ? cvt2SiteKey(newVal) : undefined) + }, () => searchedHosts.value?.map(renderOption)) ) } }) diff --git a/src/app/components/site-manage/modify/index.ts b/src/app/components/site-manage/modify/index.ts index 5085ec8c7..50bae2169 100644 --- a/src/app/components/site-manage/modify/index.ts +++ b/src/app/components/site-manage/modify/index.ts @@ -8,23 +8,23 @@ import type { Ref, SetupContext, UnwrapRef } from "vue" import { ElButton, ElDialog, ElForm, ElMessage } from "element-plus" -import { computed, defineComponent, h, reactive, ref } from "vue" +import { defineComponent, h, reactive, ref } from "vue" import { t } from "@app/locale" import { Check } from "@element-plus/icons-vue" -import hostAliasDatabase from "@service/host-alias-service" +import siteService from "@service/site-service" import SiteManageHostFormItem from "./host-form-item" -import SiteManageNameFormItem from "./name-form-item" +import SiteManageAliasFormItem from "./alias-form-item" declare type _FormData = { /** * Value of alias key */ - key: timer.site.AliasKey - name: string + key: timer.site.SiteKey + alias: string } const formRule = { - name: [ + alias: [ { required: true, message: t(msg => msg.siteManage.form.emptyAlias), @@ -49,58 +49,62 @@ function validateForm(formRef: Ref): Promise { }) } -async function handleSave(ctx: SetupContext<_Emit>, isNew: boolean, formData: _FormData): Promise { - const aliasKey = formData.key - const name = formData.name?.trim() - if (isNew && await hostAliasDatabase.exist(aliasKey)) { +async function handleAdd(siteInfo: timer.site.SiteInfo): Promise { + const existed = await siteService.exist(siteInfo) + if (existed) { ElMessage({ type: 'warning', - message: t(msg => msg.siteManage.msg.hostExistWarn, { host: aliasKey.host }), + message: t(msg => msg.siteManage.msg.hostExistWarn, { host: siteInfo.host }), showClose: true, duration: 1600 }) - return false + } else { + await siteService.add(siteInfo) } - await hostAliasDatabase.change(aliasKey, name) - ElMessage.success(t(msg => msg.siteManage.msg.saved)) - ctx.emit("save", isNew, aliasKey, name) - return true + return !existed +} + +async function handleSave(ctx: SetupContext<_Emit>, formData: _FormData): Promise { + const siteKey = formData.key + const alias = formData.alias?.trim() + const siteInfo: timer.site.SiteInfo = { ...siteKey, alias } + const success = await handleAdd(siteInfo) + if (success) { + ElMessage.success(t(msg => msg.siteManage.msg.saved)) + ctx.emit("save", siteKey, alias) + } + return success } type _Emit = { - save: (isNew: boolean, aliasKey: timer.site.AliasKey, name: string) => void + save: (siteKey: timer.site.SiteKey, name: string) => void } const BTN_ADD_TXT = t(msg => msg.siteManage.button.add) -const BTN_MDF_TXT = t(msg => msg.siteManage.button.modify) + +function initData(): _FormData { + return { + key: undefined, + alias: undefined, + } +} const _default = defineComponent({ name: "HostAliasModify", emits: { - save: () => true + save: (_siteKey: timer.site.SiteKey, _name: string) => true }, - setup: (_, context: SetupContext<_Emit>) => { - const isNew: Ref = ref(false) + setup: (_, ctx: SetupContext<_Emit>) => { const visible: Ref = ref(false) - const buttonText = computed(() => isNew ? BTN_ADD_TXT : BTN_MDF_TXT) - const formData: UnwrapRef<_FormData> = reactive({ - key: undefined, - name: undefined - }) + const formData: UnwrapRef<_FormData> = reactive(initData()) const formRef: Ref = ref() - context.expose({ + ctx.expose({ add() { formData.key = undefined - formData.name = undefined - visible.value = true - isNew.value = true - }, - modify(hostAliasInfo: timer.site.AliasIcon) { + formData.alias = undefined + visible.value = true - formData.key = hostAliasInfo - formData.name = hostAliasInfo.name - isNew.value = false }, hide: () => visible.value = false }) @@ -109,12 +113,12 @@ const _default = defineComponent({ if (!valid) { return false } - const saved = await handleSave(context, isNew.value, formData) + const saved = await handleSave(ctx, formData) saved && (visible.value = false) } return () => h(ElDialog, { width: '450px', - title: buttonText.value, + title: BTN_ADD_TXT, modelValue: visible.value, closeOnClickModal: false, onClose: () => visible.value = false @@ -128,13 +132,12 @@ const _default = defineComponent({ // Host form item h(SiteManageHostFormItem, { modelValue: formData.key, - editing: isNew.value, - onChange: (newKey: timer.site.AliasKey) => formData.key = newKey + onChange: (newKey: timer.site.SiteKey) => formData.key = newKey }), // Name form item - h(SiteManageNameFormItem, { - modelValue: formData.name, - onInput: (newVal: string) => formData.name = newVal, + h(SiteManageAliasFormItem, { + modelValue: formData.alias, + onInput: (newVal: string) => formData.alias = newVal, onEnter: save }) ] diff --git a/src/app/components/site-manage/table/column/alias.ts b/src/app/components/site-manage/table/column/alias.ts index 84315ad7a..768c0251f 100644 --- a/src/app/components/site-manage/table/column/alias.ts +++ b/src/app/components/site-manage/table/column/alias.ts @@ -9,10 +9,22 @@ import { ElIcon, ElTableColumn, ElTooltip } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" import { InfoFilled } from "@element-plus/icons-vue" +import Editable from "@app/components/common/editable" +import siteService from "@service/site-service" const label = t(msg => msg.siteManage.column.alias) const tooltip = t(msg => msg.siteManage.column.aliasInfo) +function handleChange(newAlias: string, row: timer.site.SiteInfo) { + newAlias = newAlias?.trim?.() + row.alias = newAlias + if (!newAlias) { + siteService.removeAlias(row) + } else { + siteService.saveAlias(row, newAlias, 'USER') + } +} + const _default = defineComponent({ name: "AliasColumn", setup() { @@ -21,7 +33,10 @@ const _default = defineComponent({ minWidth: 100, align: 'center', }, { - default: ({ row }: { row: timer.site.AliasIcon }) => h('span', row.name), + default: ({ row }: { row: timer.site.SiteInfo }) => h(Editable, { + modelValue: row.alias, + onChange: (newAlias: string) => handleChange(newAlias, row) + }), header: () => { const infoTooltip = h(ElTooltip, { content: tooltip, placement: 'top' }, diff --git a/src/app/components/site-manage/table/column/host.ts b/src/app/components/site-manage/table/column/host.ts index e18fcd356..44ef0067c 100644 --- a/src/app/components/site-manage/table/column/host.ts +++ b/src/app/components/site-manage/table/column/host.ts @@ -6,11 +6,9 @@ */ import { ElTableColumn } from "element-plus" -import HostAlert from "@app/components/common/host-alert" import { defineComponent, h } from "vue" import { t } from "@app/locale" -import { labelOf } from "../../common" -import { isRemainHost } from "@util/constant/remain-host" +import HostAlert from "@app/components/common/host-alert" const label = t(msg => msg.siteManage.column.host) @@ -23,15 +21,10 @@ const _default = defineComponent({ minWidth: 120, align: 'center', }, { - default: ({ row }: { row: timer.site.AliasIcon }) => row.merged || isRemainHost(row.host) - ? h('a', - { class: 'el-link el-link--default is-underline' }, - h('span', { class: 'el-link--inner' }, labelOf(row)) - ) - : h(HostAlert, { - host: labelOf(row), - iconUrl: row.iconUrl - }) + default: ({ row }: { row: timer.site.SiteInfo }) => h(HostAlert, { + host: row.host, + clickable: false + }) }) } }) diff --git a/src/app/components/site-manage/table/column/icon.ts b/src/app/components/site-manage/table/column/icon.ts new file mode 100644 index 000000000..625c55fcd --- /dev/null +++ b/src/app/components/site-manage/table/column/icon.ts @@ -0,0 +1,21 @@ +import { t } from "@app/locale" +import { ElTableColumn } from "element-plus" +import { defineComponent, h } from "vue" + +const label = t(msg => msg.siteManage.column.icon) + +const _default = defineComponent({ + name: "SiteIcon", + setup() { + return () => h(ElTableColumn, { + prop: 'iconUrl', + label, + minWidth: 40, + align: 'center', + }, { + default: ({ row }: { row: timer.site.SiteInfo }) => row.iconUrl ? h('img', { width: 12, height: 12, src: row.iconUrl }) : '' + }) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/site-manage/table/column/operation.ts b/src/app/components/site-manage/table/column/operation.ts index 3aedced9b..6eb20995f 100644 --- a/src/app/components/site-manage/table/column/operation.ts +++ b/src/app/components/site-manage/table/column/operation.ts @@ -7,39 +7,35 @@ import type { SetupContext } from "vue" -import { ElButton, ElTableColumn } from "element-plus" +import { ElTableColumn } from "element-plus" import { t } from "@app/locale" import { defineComponent, h } from "vue" -import { Delete, Edit } from "@element-plus/icons-vue" +import { Delete } from "@element-plus/icons-vue" import PopupConfirmButton from "@app/components/common/popup-confirm-button" +import siteService from "@service/site-service" type _Emit = { - delete: (row: timer.site.AliasIcon) => void - modify: (row: timer.site.AliasIcon) => void + delete: (row: timer.site.SiteInfo) => void + modify: (row: timer.site.SiteInfo) => void } const deleteButtonText = t(msg => msg.siteManage.button.delete) -const deleteButton = (ctx: SetupContext<_Emit>, row: timer.site.AliasIcon) => h(PopupConfirmButton, { +const deleteButton = (ctx: SetupContext<_Emit>, row: timer.site.SiteInfo) => h(PopupConfirmButton, { buttonIcon: Delete, buttonType: "danger", buttonText: deleteButtonText, confirmText: t(msg => msg.siteManage.deleteConfirmMsg, { host: row.host }), - onConfirm: () => ctx.emit("delete", row) + onConfirm: async () => { + await siteService.remove(row) + ctx.emit("delete", row) + } }) -const modifyButtonText = t(msg => msg.siteManage.button.modify) -const modifyButton = (ctx: SetupContext<_Emit>, row: timer.site.AliasIcon) => h(ElButton, { - size: 'small', - type: "primary", - icon: Edit, - onClick: () => ctx.emit("modify", row) -}, () => modifyButtonText) - const label = t(msg => msg.item.operation.label) const _default = defineComponent({ name: "OperationColumn", emits: { - delete: (_row: timer.site.AliasIcon) => true, + delete: (_row: timer.site.SiteInfo) => true, modify: () => true, }, setup(_, ctx: SetupContext<_Emit>) { @@ -48,10 +44,7 @@ const _default = defineComponent({ label, align: 'center', }, { - default: ({ row }: { row: timer.site.AliasIcon }) => [ - modifyButton(ctx, row), - deleteButton(ctx, row) - ] + default: ({ row }: { row: timer.site.SiteInfo }) => deleteButton(ctx, row) }) } }) diff --git a/src/app/components/site-manage/table/column/source.ts b/src/app/components/site-manage/table/column/source.ts index e142d6f21..525f61ea0 100644 --- a/src/app/components/site-manage/table/column/source.ts +++ b/src/app/components/site-manage/table/column/source.ts @@ -28,7 +28,7 @@ const _default = defineComponent({ minWidth: 70, align: 'center', }, { - default: ({ row }: { row: timer.site.Alias }) => renderSource(row.source) + default: ({ row }: { row: timer.site.SiteInfo }) => row.source ? renderSource(row.source) : '' }) } }) diff --git a/src/app/components/site-manage/table/column/type.ts b/src/app/components/site-manage/table/column/type.ts new file mode 100644 index 000000000..cf5c34ee0 --- /dev/null +++ b/src/app/components/site-manage/table/column/type.ts @@ -0,0 +1,48 @@ +import { t } from "@app/locale" +import { ElTableColumn, ElTag } from "element-plus" +import { defineComponent, h } from "vue" + +const label = t(msg => msg.siteManage.column.type) + +const normalType = t(msg => msg.siteManage.type.normal) +const mergedType = t(msg => msg.siteManage.type.merged) +const virtualType = t(msg => msg.siteManage.type.virtual) + +function cumputeText({ merged, virtual }: timer.site.SiteInfo): string { + if (merged) { + return mergedType + } else if (virtual) { + return virtualType + } else { + return normalType + } +} + +function computeType({ merged, virtual }: timer.site.SiteInfo): 'info' | 'success' | '' { + if (merged) { + return 'info' + } else if (virtual) { + return 'success' + } else { + return '' + } +} + +const _default = defineComponent({ + name: "SiteType", + setup() { + return () => h(ElTableColumn, { + prop: 'host', + label, + minWidth: 60, + align: 'center', + }, { + default: ({ row }: { row: timer.site.SiteInfo }) => h(ElTag, { + size: 'small', + type: computeType(row), + }, () => cumputeText(row)) + }) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/site-manage/table/index.ts b/src/app/components/site-manage/table/index.ts index 233958677..e06553811 100644 --- a/src/app/components/site-manage/table/index.ts +++ b/src/app/components/site-manage/table/index.ts @@ -4,22 +4,25 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { PropType } from "vue" import { ElTable } from "element-plus" -import { defineComponent, h, PropType } from "vue" +import { defineComponent, h } from "vue" import AliasColumn from "./column/alias" import HostColumn from "./column/host" +import IconColumn from "./column/icon" +import TypeColumn from "./column/type" import SourceColumn from "./column/source" import OperationColumn from "./column/operation" const _default = defineComponent({ name: "SiteManageTable", props: { - data: Array as PropType + data: Array as PropType }, emits: { - rowDelete: (_row: timer.site.AliasIcon) => true, - rowModify: (_row: timer.site.AliasIcon) => true, + rowDelete: (_row: timer.site.SiteInfo) => true, + rowModify: (_row: timer.site.SiteInfo) => true, }, setup(props, ctx) { return () => h(ElTable, { @@ -31,11 +34,12 @@ const _default = defineComponent({ fit: true, }, () => [ h(HostColumn), + h(TypeColumn), + h(IconColumn), h(AliasColumn), h(SourceColumn), h(OperationColumn, { - onModify: (row: timer.site.AliasIcon) => ctx.emit("rowModify", row), - onDelete: (row: timer.site.AliasIcon) => ctx.emit("rowDelete", row) + onDelete: (row: timer.site.SiteInfo) => ctx.emit("rowDelete", row) }) ]) } diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts index fc108689c..75ec9fa73 100644 --- a/src/app/components/trend/components/chart/wrapper.ts +++ b/src/app/components/trend/components/chart/wrapper.ts @@ -27,7 +27,7 @@ import TooltipComponent from "@echarts/component/tooltip" import { t } from "@app/locale" import { formatPeriodCommon, formatTime, MILL_PER_DAY } from "@util/time" -import hostAliasService from "@service/host-alias-service" +import siteService from "@service/site-service" import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" import { labelOfHostInfo } from "../common" @@ -193,8 +193,8 @@ async function processSubtitle(host: TrendHostInfo) { if (!host.merged) { // If not merged, append the site name to the original subtitle // @since 0.9.0 - const hostAlias: timer.site.Alias = await hostAliasService.get(host) - const siteName = hostAlias?.name + const siteInfo: timer.site.SiteInfo = await siteService.get(host) + const siteName = siteInfo?.alias siteName && (subtitle += ` / ${siteName}`) } return subtitle diff --git a/src/app/components/trend/components/common.ts b/src/app/components/trend/components/common.ts index cb8ecc0a0..688230561 100644 --- a/src/app/components/trend/components/common.ts +++ b/src/app/components/trend/components/common.ts @@ -12,8 +12,10 @@ import { t } from "@app/locale" */ export function labelOfHostInfo(hostInfo: TrendHostInfo): string { if (!hostInfo) return '' - const { host, merged } = hostInfo + const { host, merged, virtual } = hostInfo if (!host) return '' - const mergedLabel = merged ? `[${t(msg => msg.trend.merged)}]` : '' - return `${host}${mergedLabel}` + let label = '' + merged && (label = `[${t(msg => msg.trend.merged)}]`) + virtual && (label = `[${t(msg => msg.trend.virtual)}]`) + return `${host}${label}` } diff --git a/src/app/components/trend/components/filter.ts b/src/app/components/trend/components/filter.ts index 438876d07..f9ac3de09 100644 --- a/src/app/components/trend/components/filter.ts +++ b/src/app/components/trend/components/filter.ts @@ -9,7 +9,7 @@ import type { Ref, PropType, VNode } from "vue" import { ElOption, ElSelect, ElTag } from "element-plus" import { ref, h, defineComponent } from "vue" -import timerService, { HostSet } from "@service/timer-service" +import statService, { HostSet } from "@service/stat-service" import { daysAgo } from "@util/time" import { t } from "@app/locale" import { TrendMessage } from "@i18n/message/app/trend" @@ -24,10 +24,12 @@ async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref options.push({ host, merged: false })) - domains.merged.forEach(host => options.push({ host, merged: true })) + const { origin, merged, virtual } = domains + origin.forEach(host => options.push({ host })) + merged.forEach(host => options.push({ host, merged: true })) + virtual.forEach(host => options.push({ host, virtual: true })) trendDomainOptions.value = options searching.value = false } @@ -59,17 +61,21 @@ const TIME_FORMAT_LABELS: { [key in timer.app.TimeFormat]: string } = { } function keyOfHostInfo(option: TrendHostInfo): string { - const { merged, host } = option - return (merged ? "1" : '0') + (host || '') + const { merged, virtual, host } = option + let prefix = '_' + merged && (prefix = 'm') + virtual && (prefix = 'v') + return `${prefix}${host || ''}` } function hostInfoOfKey(key: string): TrendHostInfo { - if (!key || !key.length) return { host: '', merged: false } - const merged = key.charAt(0) === '1' - return { host: key.substring(1), merged } + if (!key || !key.length) return { host: '', merged: false, virtual: false } + const prefix = key.charAt(0) + return { host: key.substring(1), merged: prefix === 'm', virtual: prefix === 'v' } } const MERGED_TAG_TXT = t(msg => msg.trend.merged) +const VIRTUAL_TAG_TXT = t(msg => msg.trend.virtual) function renderHostLabel(hostInfo: TrendHostInfo): VNode[] { const result = [ h('span', {}, hostInfo.host) @@ -77,6 +83,9 @@ function renderHostLabel(hostInfo: TrendHostInfo): VNode[] { hostInfo.merged && result.push( h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) ) + hostInfo.virtual && result.push( + h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT) + ) return result } diff --git a/src/app/components/trend/index.ts b/src/app/components/trend/index.ts index 287bfa5bf..df4c9637e 100644 --- a/src/app/components/trend/index.ts +++ b/src/app/components/trend/index.ts @@ -13,7 +13,7 @@ import { daysAgo } from "@util/time" import ContentContainer from "../common/content-container" import TrendChart from "./components/chart" import TrendFilter from "./components/filter" -import timerService, { TimerQueryParam } from "@service/timer-service" +import statService, { StatQueryParam } from "@service/stat-service" type _Queries = { host: string @@ -34,7 +34,7 @@ async function query(hostOption: Ref, dateRange: Ref): Pr if (!hostVal) { return [] } - const param: TimerQueryParam = { + const param: StatQueryParam = { // If the host is empty, no result will be queried with this param. host: hostVal, mergeHost: hostOption.value?.merged || false, @@ -43,7 +43,7 @@ async function query(hostOption: Ref, dateRange: Ref): Pr sort: 'date', sortOrder: 'ASC' } - return await timerService.select(param) + return await statService.select(param) } const _default = defineComponent({ diff --git a/src/app/components/trend/trend.d.ts b/src/app/components/trend/trend.d.ts index 40a178c0b..69fc5c1e4 100644 --- a/src/app/components/trend/trend.d.ts +++ b/src/app/components/trend/trend.d.ts @@ -1,6 +1,7 @@ declare type TrendHostInfo = { host: string - merged: boolean + merged?: boolean + virtual?: boolean } declare type TrendFilterOption = { diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 3afe77570..1bb13eab6 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -19,6 +19,7 @@ import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/consta import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Memo, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" +import { createTab } from "@api/chrome/tab" type _MenuItem = { title: keyof MenuMessage @@ -131,9 +132,7 @@ function openMenu(route: string, title: I18nKey, routeProps: UnwrapRef<_RoutePro } } -const openHref = (href: string) => { - chrome.tabs.create({ url: href }) -} +const openHref = (href: string) => createTab(href) function handleClick(_MenuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { const { route, title, href } = _MenuItem diff --git a/src/background/active-tab-listener.ts b/src/background/active-tab-listener.ts index 427d36afe..4bcccee14 100644 --- a/src/background/active-tab-listener.ts +++ b/src/background/active-tab-listener.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { getTab, onTabActivated } from "@api/chrome/tab" import { extractHostname, HostInfo } from "@util/pattern" type _Param = { @@ -18,7 +19,7 @@ type _Handler = (params: _Param) => void export default class ActiveTabListener { listener: Array<_Handler> = [] - private async processWithTabInfo({ url, id }: chrome.tabs.Tab) { + private async processWithTabInfo({ url, id }: ChromeTab) { const hostInfo: HostInfo = extractHostname(url) const host: string = hostInfo.host const param: _Param = { url, tabId: id, host } @@ -31,8 +32,9 @@ export default class ActiveTabListener { } listen() { - chrome.tabs.onActivated.addListener((activeInfo: chrome.tabs.TabActiveInfo) => { - chrome.tabs.get(activeInfo.tabId, tab => this.processWithTabInfo(tab)) + onTabActivated(async tabId => { + const tab = await getTab(tabId) + this.processWithTabInfo(tab) }) } } diff --git a/src/background/alarm-manager.ts b/src/background/alarm-manager.ts index 2abb14e56..10315f65a 100644 --- a/src/background/alarm-manager.ts +++ b/src/background/alarm-manager.ts @@ -1,11 +1,14 @@ +import { clearAlarm, createAlarm, onAlarm } from "@api/chrome/alarm" +import { getRuntimeId } from "@api/chrome/runtime" + type _AlarmConfig = { handler: _Handler, interval: number, } -type _Handler = (alarm: chrome.alarms.Alarm) => void +type _Handler = (alarm: ChromeAlarm) => void -const ALARM_PREFIX = 'timer-alarm-' + chrome.runtime.id + '-' +const ALARM_PREFIX = 'timer-alarm-' + getRuntimeId() + '-' const ALARM_PREFIX_LENGTH = ALARM_PREFIX.length const getInnerName = (outerName: string) => ALARM_PREFIX + outerName @@ -24,7 +27,7 @@ class AlarmManager { } private init() { - chrome.alarms.onAlarm.addListener(alarm => { + onAlarm(async alarm => { const name = alarm.name if (!name.startsWith(ALARM_PREFIX)) { // Unknown alarm @@ -40,10 +43,8 @@ class AlarmManager { config.handler?.(alarm) const nextTs = Date.now() + config.interval // Clear this one - chrome.alarms.clear(name, (_cleared: boolean) => { - // Create new one - chrome.alarms.create(name, { when: nextTs }) - }) + await clearAlarm(name) + createAlarm(name, nextTs) }) } @@ -63,7 +64,7 @@ class AlarmManager { // Initialize config this.alarms[outerName] = config // Create new one alarm - chrome.alarms.create(getInnerName(outerName), { when: Date.now() + interval }) + createAlarm(getInnerName(outerName), Date.now() + interval) } /** @@ -71,7 +72,7 @@ class AlarmManager { */ remove(outerName: string) { delete this.alarms[outerName] - chrome.alarms.clear(getInnerName(outerName)) + clearAlarm(getInnerName(outerName)) } } diff --git a/src/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 5c993e6b3..7690f7b12 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -5,14 +5,17 @@ * https://opensource.org/licenses/MIT */ -import TimerDatabase from "@db/timer-database" +import { setBadgeText } from "@api/chrome/action" +import { listTabs } from "@api/chrome/tab" +import { getFocusedNormalWindow } from "@api/chrome/window" +import StatDatabase from "@db/stat-database" import whitelistHolder from "@service/components/whitelist-holder" import optionService from "@service/option-service" import { extractHostname, isBrowserUrl } from "@util/pattern" import alarmManager from "./alarm-manager" const storage = chrome.storage.local -const timerDb: TimerDatabase = new TimerDatabase(storage) +const statDatabase: StatDatabase = new StatDatabase(storage) export type BadgeLocation = { /** @@ -42,36 +45,19 @@ function setBadgeTextOfMills(milliseconds: number | undefined, tabId: number | u setBadgeText(text, tabId) } -function setBadgeText(text: string, tabId: number | undefined) { - chrome.browserAction?.setBadgeText?.({ text, tabId }) -} - -function findFocusedWindow(): Promise { - return new Promise(resolve => - chrome.windows.getLastFocused( - // Only find normal window - { windowTypes: ['normal'] }, - window => resolve(window && window.focused ? window : undefined) - ) - ) -} - async function findActiveTab(): Promise { - const window = await findFocusedWindow() + const window = await getFocusedNormalWindow() if (!window) { return undefined } - return new Promise(resolve => chrome.tabs.query({ active: true, windowId: window.id }, tabs => { - // Fix #131 - // Edge will return two active tabs, including the new tab with url 'edge://newtab/', GG - tabs = tabs.filter(tab => !isBrowserUrl(tab.url)) - if (!tabs || !tabs.length) { - resolve(undefined) - } else { - const { url, id } = tabs[0] - resolve({ tabId: id, url }) - } - })) + const tabs = await listTabs({ active: true, windowId: window.id }) + // Fix #131 + // Edge will return two active tabs, including the new tab with url 'edge://newtab/', GG + const tab = tabs.filter(tab => !isBrowserUrl(tab?.url))[0] + if (!tab) { + return undefined + } + return { tabId: tab.id, url: tab.url } } async function updateFocus(badgeLocation?: BadgeLocation, lastLocation?: BadgeLocation): Promise { @@ -90,7 +76,7 @@ async function updateFocus(badgeLocation?: BadgeLocation, lastLocation?: BadgeLo setBadgeText('W', tabId) return badgeLocation } - const milliseconds = host ? (await timerDb.get(host, new Date())).focus : undefined + const milliseconds = host ? (await statDatabase.get(host, new Date())).focus : undefined setBadgeTextOfMills(milliseconds, tabId) return badgeLocation } diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 0d4fb41bc..7e288d679 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -6,17 +6,21 @@ */ import { OPTION_ROUTE } from "../app/router/constants" -import { getAppPageUrl, getGuidePageUrl, SOURCE_CODE_PAGE, TU_CAO_PAGE } from "@util/constant/url" +import { getAppPageUrl, getGuidePageUrl, GITHUB_ISSUE_ADD, SOURCE_CODE_PAGE, TU_CAO_PAGE } from "@util/constant/url" import { t2Chrome } from "@i18n/chrome/t" -import { IS_SAFARI } from "@util/constant/environment" +import { IS_MV3, IS_SAFARI } from "@util/constant/environment" +import { createTab } from "@api/chrome/tab" +import { createContextMenu } from "@api/chrome/context-menu" +import { getRuntimeId } from "@api/chrome/runtime" +import { locale } from "@i18n" const APP_PAGE_URL = getAppPageUrl(true) -const baseProps: Partial = { +const baseProps: Partial = { // Cast unknown to fix the error with manifestV2 // Because 'browser_action' will be replaced with 'action' in union type chrome.contextMenus.ContextType since V3 // But 'action' does not work in V2 - contexts: ['browser_action'] as unknown as chrome.contextMenus.ContextType[], + contexts: [IS_MV3 ? 'action' : 'browser_action'], visible: true } @@ -29,55 +33,47 @@ function titleOf(prefixEmoji: string, title: string) { } } -const allFunctionProps: chrome.contextMenus.CreateProperties = { - id: chrome.runtime.id + '_timer_menu_item_app_link', +const allFunctionProps: ChromeContextMenuCreateProps = { + id: getRuntimeId() + '_timer_menu_item_app_link', title: titleOf('🏷️', t2Chrome(msg => msg.base.allFunction)), - onclick: () => chrome.tabs.create({ url: APP_PAGE_URL }), + onclick: () => createTab(APP_PAGE_URL), ...baseProps } -const optionPageProps: chrome.contextMenus.CreateProperties = { - id: chrome.runtime.id + '_timer_menu_item_option_link', +const optionPageProps: ChromeContextMenuCreateProps = { + id: getRuntimeId() + '_timer_menu_item_option_link', title: titleOf('🥰', t2Chrome(msg => msg.contextMenus.optionPage)), - onclick: () => chrome.tabs.create({ url: APP_PAGE_URL + '#' + OPTION_ROUTE }), + onclick: () => createTab(APP_PAGE_URL + '#' + OPTION_ROUTE), ...baseProps } -const repoPageProps: chrome.contextMenus.CreateProperties = { - id: chrome.runtime.id + '_timer_menu_item_repo_link', +const repoPageProps: ChromeContextMenuCreateProps = { + id: getRuntimeId() + '_timer_menu_item_repo_link', title: titleOf('🍻', t2Chrome(msg => msg.contextMenus.repoPage)), - onclick: () => chrome.tabs.create({ url: SOURCE_CODE_PAGE }), + onclick: () => createTab(SOURCE_CODE_PAGE), ...baseProps } -const feedbackPageProps: chrome.contextMenus.CreateProperties = { - id: chrome.runtime.id + '_timer_menu_item_feedback_link', +const feedbackPageProps: ChromeContextMenuCreateProps = { + id: getRuntimeId() + '_timer_menu_item_feedback_link', title: titleOf('😿', t2Chrome(msg => msg.contextMenus.feedbackPage)), - onclick: () => chrome.tabs.create({ url: TU_CAO_PAGE }), + onclick: () => createTab(locale === 'zh_CN' ? TU_CAO_PAGE : GITHUB_ISSUE_ADD), ...baseProps } -const guidePageProps: chrome.contextMenus.CreateProperties = { - id: chrome.runtime.id + '_timer_menu_item_guide_link', +const guidePageProps: ChromeContextMenuCreateProps = { + id: getRuntimeId() + '_timer_menu_item_guide_link', title: titleOf('📖', t2Chrome(msg => msg.base.guidePage)), - onclick: () => chrome.tabs.create({ url: getGuidePageUrl(true) }), + onclick: () => createTab(getGuidePageUrl(true)), ...baseProps } function init() { - create(allFunctionProps) - create(optionPageProps) - create(repoPageProps) - create(feedbackPageProps) - create(guidePageProps) -} - -function create(props: chrome.contextMenus.CreateProperties) { - chrome.contextMenus.create(props, () => { - const error: chrome.runtime.LastError = chrome.runtime.lastError - const duplicated = error?.message?.startsWith('Cannot create item with duplicate id') - duplicated && console.log("Duplicated item: " + props.id) - }) + createContextMenu(allFunctionProps) + createContextMenu(optionPageProps) + createContextMenu(repoPageProps) + createContextMenu(feedbackPageProps) + createContextMenu(guidePageProps) } export default init diff --git a/src/background/content-script-handler.ts b/src/background/content-script-handler.ts index 0590627bd..99d079a86 100644 --- a/src/background/content-script-handler.ts +++ b/src/background/content-script-handler.ts @@ -8,7 +8,7 @@ import TimeLimitItem from "@entity/time-limit-item" import limitService from "@service/limit-service" import optionService from "@service/option-service" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import whitelistService from "@service/whitelist-service" import MessageDispatcher from "./message-dispatcher" @@ -21,7 +21,7 @@ export default function init(dispatcher: MessageDispatcher) { dispatcher // Increase the visit time .register('cs.incVisitCount', async host => { - timerService.addOneTime(host) + statService.addOneTime(host) }) // Judge is in whitelist .register('cs.isInWhitelist', host => whitelistService.include(host)) @@ -33,7 +33,7 @@ export default function init(dispatcher: MessageDispatcher) { // Get today info .register('cs.getTodayInfo', host => { const now = new Date() - return timerService.getResult(host, now) + return statService.getResult(host, now) }) // More minutes .register('cs.moreMinutes', url => limitService.moreMinutes(url)) diff --git a/src/background/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index 499ceae66..b80cb70c0 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -5,18 +5,16 @@ * https://opensource.org/licenses/MIT */ -import HostAliasDatabase from "@db/host-alias-database" -import IconUrlDatabase from "@db/icon-url-database" import OptionDatabase from "@db/option-database" import { IS_CHROME, IS_SAFARI } from "@util/constant/environment" import { iconUrlOfBrowser } from "@util/constant/url" import { extractHostname, isBrowserUrl, isHomepage } from "@util/pattern" import { defaultStatistics } from "@util/constant/option" import { extractSiteName } from "@util/site" +import { getTab } from "@api/chrome/tab" +import siteService from "@service/site-service" const storage: chrome.storage.StorageArea = chrome.storage.local -const iconUrlDatabase = new IconUrlDatabase(storage) -const hostAliasDatabase = new HostAliasDatabase(storage) const optionDatabase = new OptionDatabase(storage) let collectAliasEnabled = defaultStatistics().collectSiteName @@ -28,17 +26,17 @@ function isUrl(title: string) { return title.startsWith('https://') || title.startsWith('http://') || title.startsWith('ftp://') } -function collectAlias(host: string, tabTitle: string) { +async function collectAlias(key: timer.site.SiteKey, tabTitle: string) { if (isUrl(tabTitle)) return if (!tabTitle) return - const siteName = extractSiteName(tabTitle, host) - siteName && hostAliasDatabase.update({ name: siteName, host, source: 'DETECTED' }) + const siteName = extractSiteName(tabTitle, key.host) + siteName && await siteService.saveAlias(key, siteName, 'DETECTED') } /** * Process the tab */ -async function processTabInfo(tab: chrome.tabs.Tab): Promise { +async function processTabInfo(tab: ChromeTab): Promise { if (!tab) return const url = tab.url if (!url) return @@ -50,20 +48,25 @@ async function processTabInfo(tab: chrome.tabs.Tab): Promise { let favIconUrl = tab.favIconUrl // localhost hosts with Chrome use cache, so keep the favIcon url undefined IS_CHROME && /^localhost(:.+)?/.test(host) && (favIconUrl = undefined) + const siteKey: timer.site.SiteKey = { host } const iconUrl = favIconUrl || iconUrlOfBrowser(protocol, host) - iconUrl && iconUrlDatabase.put(host, iconUrl) - collectAliasEnabled && !isBrowserUrl(url) && isHomepage(url) && collectAlias(host, tab.title) + iconUrl && siteService.saveIconUrl(siteKey, iconUrl) + collectAliasEnabled + && !isBrowserUrl(url) + && isHomepage(url) + && collectAlias(siteKey, tab.title) } /** * Fire when the web navigation completed */ -function handleWebNavigationCompleted(detail: chrome.webNavigation.WebNavigationFramedCallbackDetails) { +async function handleWebNavigationCompleted(detail: chrome.webNavigation.WebNavigationFramedCallbackDetails) { if (detail.frameId > 0) { // we don't care about activity occurring within a sub frame of a tab return } - chrome.tabs.get(detail.tabId, processTabInfo) + const tab = await getTab(detail?.tabId) + tab && processTabInfo(tab) } function listen() { diff --git a/src/background/index.ts b/src/background/index.ts index 6abeb382d..bc23afe36 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -21,6 +21,9 @@ import initLimitProcesser from "./limit-processor" import initCsHandler from "./content-script-handler" import { isBrowserUrl } from "@util/pattern" import BackupScheduler from "./backup-scheduler" +import { createTab, listTabs } from "@api/chrome/tab" +import { isNoneWindowId, onNormalWindowFocusChanged } from "@api/chrome/window" +import { onInstalled } from "@api/chrome/runtime" // Open the log of console openLog() @@ -60,20 +63,17 @@ new ActiveTabListener() .listen() // Listen window focus changed -chrome.windows.onFocusChanged.addListener(windowId => { - if (windowId === chrome.windows.WINDOW_ID_NONE) { - return - } - chrome.tabs.query({ windowId }, tabs => tabs - .filter(tab => tab.active && !isBrowserUrl(tab.url)) +onNormalWindowFocusChanged(async windowId => { + if (isNoneWindowId(windowId)) return + const tabs = await listTabs({ windowId, active: true }) + tabs.filter(tab => !isBrowserUrl(tab?.url)) .forEach(({ url, id }) => badgeTextManager.forceUpdate({ url, tabId: id })) - ) }) // Collect the install time -chrome.runtime.onInstalled.addListener(async detail => { - if (detail.reason === "install") { - chrome.tabs.create({ url: getGuidePageUrl(true) }) +onInstalled(async reason => { + if (reason === "install") { + createTab(getGuidePageUrl(true)) await metaService.updateInstallTime(new Date()) } // Questionnaire for uninstall diff --git a/src/background/limit-processor.ts b/src/background/limit-processor.ts index 39f9499f6..bc383badd 100644 --- a/src/background/limit-processor.ts +++ b/src/background/limit-processor.ts @@ -5,46 +5,35 @@ * https://opensource.org/licenses/MIT */ +import { createTab, listTabs, sendMsg2Tab } from "@api/chrome/tab" import { LIMIT_ROUTE } from "@app/router/constants" import TimeLimitItem from "@entity/time-limit-item" import { getAppPageUrl } from "@util/constant/url" import MessageDispatcher from "./message-dispatcher" -function processLimitWaking(rules: TimeLimitItem[], tab: chrome.tabs.Tab) { +function processLimitWaking(rules: TimeLimitItem[], tab: ChromeTab) { const { url } = tab const anyMatch = rules.map(rule => rule.matches(url)).reduce((a, b) => a || b, false) if (!anyMatch) { return } - chrome.tabs.sendMessage, timer.mq.Response>(tab.id, { - code: "limitWaking", - data: rules - }, result => { - if (result?.code === "fail") { - console.error(`Failed to wake with limit rule: rules=${JSON.stringify(rules)}, msg=${result.msg}`) - } else if (result?.code === "success") { - console.log(`Waked tab[id=${tab.id}]`) - } - }) + sendMsg2Tab(tab.id, 'limitWaking', rules) + .then(() => console.log(`Waked tab[id=${tab.id}]`)) + .catch(err => console.error(`Failed to wake with limit rule: rules=${JSON.stringify(rules)}, msg=${err.msg}`)) } export default function init(dispatcher: MessageDispatcher) { dispatcher .register( 'openLimitPage', - async (url: string) => { - const pageUrl = getAppPageUrl(true, LIMIT_ROUTE, { url: encodeURI(url) }) - chrome.tabs.create({ url: pageUrl }) - } + (url: string) => createTab(getAppPageUrl(true, LIMIT_ROUTE, { url: encodeURI(url) })) ) .register( 'limitWaking', async data => { - const rules = (data || []) - .map(like => TimeLimitItem.of(like)) - chrome.tabs.query({ status: "complete" }, tabs => { - tabs.forEach(tab => processLimitWaking(rules, tab)) - }) + const rules = data?.map(like => TimeLimitItem.of(like)) || [] + const tabs = await listTabs({ status: 'complete' }) + tabs.forEach(tab => processLimitWaking(rules, tab)) } ) } \ No newline at end of file diff --git a/src/background/message-dispatcher.ts b/src/background/message-dispatcher.ts index 42dc973cb..0362974cb 100644 --- a/src/background/message-dispatcher.ts +++ b/src/background/message-dispatcher.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { log } from "@src/common/logger" +import { onRuntimeMessage } from "@api/chrome/runtime" class MessageDispatcher { private handlers: Partial<{ @@ -20,7 +20,7 @@ class MessageDispatcher { return this } - private async handle(message: timer.mq.Request): Promise> { + private async handle(message: timer.mq.Request, sender: ChromeMessageSender): Promise> { const code = message?.code if (!code) { return { code: 'ignore' } @@ -30,7 +30,7 @@ class MessageDispatcher { return { code: 'ignore' } } try { - const result = await handler(message.data) + const result = await handler(message.data, sender) return { code: 'success', data: result } } catch (error) { return { code: 'fail', msg: error } @@ -38,18 +38,7 @@ class MessageDispatcher { } start() { - // Be careful!!! - // Can't use await/async in callback parameter - chrome.runtime.onMessage.addListener((message: timer.mq.Request, _sender: never, sendResponse: timer.mq.Callback) => { - log('start to handle message', message.code, message.data) - this.handle(message).then(response => { - log('the response is', response, message) - sendResponse(response) - }) - // 'return ture' will force chrome to wait for the response processed in the above promise. - // @see https://github.com/mozilla/webextension-polyfill/issues/130 - return true - }) + onRuntimeMessage((msg, sender) => this.handle(msg, sender)) } } diff --git a/src/background/timer/collector.ts b/src/background/timer/collector.ts index 3878b6dfa..130337188 100644 --- a/src/background/timer/collector.ts +++ b/src/background/timer/collector.ts @@ -8,20 +8,14 @@ import { isBrowserUrl, extractHostname, extractFileHost } from "@util/pattern" import CollectionContext from "./collection-context" import optionService from "@service/option-service" +import { listTabs } from "@api/chrome/tab" +import { listAllWindows } from "@api/chrome/window" let countLocalFiles: boolean optionService.getAllOption().then(option => countLocalFiles = !!option.countLocalFiles) optionService.addOptionChangeListener((newVal => countLocalFiles = !!newVal.countLocalFiles)) -function queryAllWindows(): Promise { - return new Promise(resolve => chrome.windows.getAll(resolve)) -} - -function queryAllTabs(windowId: number): Promise { - return new Promise(resolve => chrome.tabs.query({ windowId }, resolve)) -} - -function handleTab(tab: chrome.tabs.Tab, window: chrome.windows.Window, context: CollectionContext) { +function handleTab(tab: ChromeTab, window: ChromeWindow, context: CollectionContext) { if (!tab.active || !window.focused) { return } @@ -41,9 +35,9 @@ function handleTab(tab: chrome.tabs.Tab, window: chrome.windows.Window, context: } async function doCollect(context: CollectionContext) { - const windows = await queryAllWindows() + const windows = await listAllWindows() for (const window of windows) { - const tabs = await queryAllTabs(window.id) + const tabs = await listTabs({ windowId: window.id }) // tabs maybe undefined if (!tabs) { continue diff --git a/src/background/timer/save.ts b/src/background/timer/save.ts index 7467a78fa..74d6ae1be 100644 --- a/src/background/timer/save.ts +++ b/src/background/timer/save.ts @@ -5,34 +5,26 @@ * https://opensource.org/licenses/MIT */ +import { listTabs, sendMsg2Tab } from "@api/chrome/tab" import limitService from "@service/limit-service" import periodService from "@service/period-service" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import { sum } from "@util/array" import CollectionContext from "./collection-context" -function sendLimitedMessage(item: timer.limit.Item[]) { - chrome.tabs.query({ status: "complete" }, tabs => { - tabs.forEach(tab => { - chrome.tabs.sendMessage, timer.mq.Response>(tab.id, { - code: "limitTimeMeet", - data: item - }, result => { - if (result?.code === "fail") { - console.error(`Failed to execute limit rule: rule=${JSON.stringify(item)}, msg=${result.msg}`) - } else if (result?.code === "success") { - console.log(`Processed limit rules: rule=${JSON.stringify(item)}`) - } - }) - }) - }) +async function sendLimitedMessage(item: timer.limit.Item[]) { + const tabs = await listTabs({ status: 'complete' }) + tabs.forEach(tab => sendMsg2Tab(tab.id, 'limitTimeMeet', item) + .then(() => console.log(`Processed limit rules: rule=${JSON.stringify(item)}`)) + .catch(err => console.error(`Failed to execute limit rule: rule=${JSON.stringify(item)}, msg=${err.msg}`)) + ) } export default async function save(collectionContext: CollectionContext) { const context = collectionContext.timerContext if (context.isPaused()) return const timeMap = context.timeMap - timerService.addFocusAndTotal(timeMap) + statService.addFocusTime(timeMap) const totalFocusTime = sum(Object.values(timeMap).map(timeInfo => sum(Object.values(timeInfo)))) // Add period time await periodService.add(context.lastCollectTime, totalFocusTime) diff --git a/src/background/timer/timer.d.ts b/src/background/timer/timer.d.ts index 80faa79c2..35bf0c132 100644 --- a/src/background/timer/timer.d.ts +++ b/src/background/timer/timer.d.ts @@ -1,4 +1,4 @@ declare type TimeInfo = { - [url: string]: number // Focus time + [urlOfHost: string]: number // Focus time } \ No newline at end of file diff --git a/src/background/uninstall-listener.ts b/src/background/uninstall-listener.ts index f9ef479cd..438ff070a 100644 --- a/src/background/uninstall-listener.ts +++ b/src/background/uninstall-listener.ts @@ -7,11 +7,12 @@ import { UNINSTALL_QUESTIONNAIRE } from "@util/constant/url" import { locale } from "@i18n" +import { setUninstallURL } from "@api/chrome/runtime" async function listen() { try { const uninstallUrl = UNINSTALL_QUESTIONNAIRE[locale] - uninstallUrl && chrome.runtime.setUninstallURL(uninstallUrl) + uninstallUrl && setUninstallURL(uninstallUrl) } catch (e) { console.error(e) } diff --git a/src/background/version-manager/0-7-0/local-file-initializer.ts b/src/background/version-manager/0-7-0/local-file-initializer.ts index 6bd42f0b2..617ca3491 100644 --- a/src/background/version-manager/0-7-0/local-file-initializer.ts +++ b/src/background/version-manager/0-7-0/local-file-initializer.ts @@ -6,14 +6,12 @@ */ import MergeRuleDatabase from "@db/merge-rule-database" -import HostAliasDatabase from "@db/host-alias-database" import { JSON_HOST, LOCAL_HOST_PATTERN, MERGED_HOST, PDF_HOST, PIC_HOST, TXT_HOST } from "@util/constant/remain-host" import { t2Chrome } from "@i18n/chrome/t" +import siteService from "@service/site-service" const storage: chrome.storage.StorageArea = chrome.storage.local - const mergeRuleDatabase = new MergeRuleDatabase(storage) -const hostAliasDatabase = new HostAliasDatabase(storage) /** * Process the host of local files @@ -32,25 +30,25 @@ export default class LocalFileInitializer implements VersionProcessor { merged: MERGED_HOST, }).then(() => console.log('Local file merge rules initialized')) // Add site name - hostAliasDatabase.update({ - host: PDF_HOST, - name: t2Chrome(msg => msg.initial.localFile.pdf), - source: 'DETECTED' - }) - hostAliasDatabase.update({ - host: JSON_HOST, - name: t2Chrome(msg => msg.initial.localFile.json), - source: 'DETECTED' - }) - hostAliasDatabase.update({ - host: PIC_HOST, - name: t2Chrome(msg => msg.initial.localFile.pic), - source: 'DETECTED' - }) - hostAliasDatabase.update({ - host: TXT_HOST, - name: t2Chrome(msg => msg.initial.localFile.txt), - source: 'DETECTED' - }) + siteService.saveAlias( + { host: PDF_HOST }, + t2Chrome(msg => msg.initial.localFile.pdf), + 'DETECTED' + ) + siteService.saveAlias( + { host: JSON_HOST }, + t2Chrome(msg => msg.initial.localFile.json), + 'DETECTED' + ) + siteService.saveAlias( + { host: PIC_HOST }, + t2Chrome(msg => msg.initial.localFile.pic), + 'DETECTED' + ) + siteService.saveAlias( + { host: TXT_HOST }, + t2Chrome(msg => msg.initial.localFile.txt), + 'DETECTED' + ) } } \ No newline at end of file diff --git a/src/background/version-manager/1-4-3/running-time-clear.ts b/src/background/version-manager/1-4-3/running-time-clear.ts deleted file mode 100644 index cb7b9a1de..000000000 --- a/src/background/version-manager/1-4-3/running-time-clear.ts +++ /dev/null @@ -1,38 +0,0 @@ -import TimerDatabase from "@db/timer-database" -import { isNotZeroResult } from "@util/stat" - -const db = new TimerDatabase(chrome.storage.local) - -/** - * Clear the running time - * - * @since 1.4.3 - */ -export default class RunningTimeClear implements VersionProcessor { - - since(): string { - return "1.4.3" - } - - async process(reason: chrome.runtime.OnInstalledReason): Promise { - // Only trigger when updating - if (reason !== 'update') { - return - } - const allRows = await db.select() - const rows2Delete: timer.stat.Row[] = [] - let updatedCount = 0 - for (const row of allRows) { - if (isNotZeroResult(row)) { - // force update - await db.forceUpdate(row) - updatedCount++ - } else { - // delete it - rows2Delete.push(row) - } - } - await db.delete(rows2Delete) - console.log(`Updated ${updatedCount} rows, deleted ${rows2Delete.length} rows`) - } -} \ No newline at end of file diff --git a/src/background/version-manager/1-6-0/alias-icon-cleaner.ts b/src/background/version-manager/1-6-0/alias-icon-cleaner.ts new file mode 100644 index 000000000..c0d97b8c2 --- /dev/null +++ b/src/background/version-manager/1-6-0/alias-icon-cleaner.ts @@ -0,0 +1,46 @@ +import HostAliasDatabase from "@db/host-alias-database" +import IconUrlDatabase from "@db/icon-url-database" +import siteService from "@service/site-service" + +const storage = chrome.storage.local +const aliasDatabase = new HostAliasDatabase(storage) +const iconDatabase = new IconUrlDatabase(storage) + +/** + * Merge alias and icon to site + * + * @since 1.6.0 + */ +export default class AliasIconCleaner implements VersionProcessor { + + since(): string { + return "1.6.0" + } + + async process(reason: ChromeOnInstalledReason): Promise { + // Only trigger when updating + if (reason !== 'update') { + return + } + const hostIcons = await iconDatabase.listAll() + const iconResults = await Promise.all( + Object.entries(hostIcons).map( + async ([host, iconUrl]) => { + await siteService.saveIconUrl({ host }, iconUrl) + await iconDatabase.remove(host) + } + ) + ) + + const alias = await aliasDatabase.selectAll() + const aliasResults = await Promise.all( + alias.map( + async site => { + await siteService.saveAlias(site, site.alias, site.source) + await aliasDatabase.remove(site) + } + ) + ) + console.log(`Merge ${iconResults?.length} icons, ${aliasResults?.length} alias`) + } +} \ No newline at end of file diff --git a/src/background/version-manager/index.ts b/src/background/version-manager/index.ts index 9378ccda1..87eb5b534 100644 --- a/src/background/version-manager/index.ts +++ b/src/background/version-manager/index.ts @@ -5,9 +5,10 @@ * https://opensource.org/licenses/MIT */ +import { getVersion, onInstalled } from "@api/chrome/runtime" import HostMergeInitializer from "./0-1-2/host-merge-initializer" import LocalFileInitializer from "./0-7-0/local-file-initializer" -import RunningTimeClear from "./1-4-3/running-time-clear" +import RunningTimeClear from "./1-6-0/alias-icon-cleaner" /** * Version manager @@ -26,8 +27,8 @@ class VersionManager { this.processorChain = this.processorChain.sort((a, b) => a.since() >= b.since() ? 1 : 0) } - private onChromeInstalled(reason: chrome.runtime.OnInstalledReason) { - const version: string = chrome.runtime.getManifest().version + private onChromeInstalled(reason: ChromeOnInstalledReason) { + const version: string = getVersion() if (reason === 'update') { // Update, process the latest version, which equals to current version this.processorChain @@ -40,7 +41,7 @@ class VersionManager { } init() { - chrome.runtime.onInstalled.addListener(detail => this.onChromeInstalled(detail.reason)) + onInstalled(reason => this.onChromeInstalled(reason)) } } diff --git a/src/background/version-manager/version-manager.d.ts b/src/background/version-manager/version-manager.d.ts index 18ca6519b..63baa3c33 100644 --- a/src/background/version-manager/version-manager.d.ts +++ b/src/background/version-manager/version-manager.d.ts @@ -21,5 +21,5 @@ declare type VersionProcessor = { * * @param reason reason of chrome OnInstalled event */ - process(reason: chrome.runtime.OnInstalledReason): void + process(reason: ChromeOnInstalledReason): void } \ No newline at end of file diff --git a/src/background/whitelist-menu-manager.ts b/src/background/whitelist-menu-manager.ts index bfe18e6b5..9eca26227 100644 --- a/src/background/whitelist-menu-manager.ts +++ b/src/background/whitelist-menu-manager.ts @@ -10,6 +10,8 @@ import optionService from "@service/option-service" import { t2Chrome } from "@i18n/chrome/t" import { ContextMenusMessage } from "@i18n/message/common/context-menus" import { extractHostname, isBrowserUrl } from "@util/pattern" +import { getTab, onTabActivated, onTabUpdated } from "@api/chrome/tab" +import { createContextMenu, updateContextMenu } from "@api/chrome/context-menu" const db = new WhitelistDatabase(chrome.storage.local) @@ -22,7 +24,7 @@ let visible = true const removeOrAdd = (removeOrAddFlag: boolean, host: string) => removeOrAddFlag ? db.remove(host) : db.add(host) -const menuInitialOptions: chrome.contextMenus.CreateProperties = { +const menuInitialOptions: ChromeContextMenuCreateProps = { contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio'], id: menuId, checked: true, @@ -30,15 +32,16 @@ const menuInitialOptions: chrome.contextMenus.CreateProperties = { visible: false } -function updateContextMenu(currentActiveTab: chrome.tabs.Tab | number) { - if (typeof currentActiveTab === 'number') { +async function updateContextMenuInner(param: ChromeTab | number) { + if (typeof param === 'number') { // If number, get the tabInfo first - chrome.tabs.get(currentActiveId, tab => tab && updateContextMenu(tab)) + const tab: ChromeTab = await getTab(currentActiveId) + tab && updateContextMenuInner(tab) } else { - const tab = currentActiveTab as chrome.tabs.Tab + const tab = param as ChromeTab const { url } = tab const targetHost = url && !isBrowserUrl(url) ? extractHostname(tab.url).host : '' - const changeProp: chrome.contextMenus.UpdateProperties = {} + const changeProp: ChromeContextMenuUpdateProps = {} if (!targetHost) { // If not a valid host, hide this menu changeProp.visible = false @@ -50,29 +53,28 @@ function updateContextMenu(currentActiveTab: chrome.tabs.Tab | number) { changeProp.title = t2Chrome(root => root.contextMenus[titleMsgField]).replace('{host}', targetHost) changeProp.onclick = () => removeOrAdd(existsInWhitelist, targetHost) } - chrome.contextMenus.update(menuId, changeProp) + updateContextMenu(menuId, changeProp) } } const handleListChange = (newWhitelist: string[]) => { whitelist = newWhitelist - updateContextMenu(currentActiveId) + updateContextMenuInner(currentActiveId) } -const handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: number | chrome.tabs.Tab) => { - if (chrome.runtime.lastError) { /** prevent it from throwing error */ } +const handleTabUpdated = (tabId: number, changeInfo: ChromeTabChangeInfo, tab: number | ChromeTab) => { // Current active tab updated tabId === currentActiveId && changeInfo.status === 'loading' - && updateContextMenu(tab) + && updateContextMenuInner(tab) } -const handleTabActivated = (activeInfo: chrome.tabs.TabActiveInfo) => updateContextMenu(currentActiveId = activeInfo.tabId) +const handleTabActivated = (activeInfo: ChromeTabActiveInfo) => updateContextMenuInner(currentActiveId = activeInfo.tabId) async function init() { - chrome.contextMenus.create(menuInitialOptions) - chrome.tabs.onUpdated.addListener(handleTabUpdated) - chrome.tabs.onActivated.addListener(handleTabActivated) + createContextMenu(menuInitialOptions) + onTabUpdated(handleTabUpdated) + onTabActivated((_tabId, activeInfo) => handleTabActivated(activeInfo)) whitelist = await db.selectAll() db.addChangeListener(handleListChange) visible = (await optionService.getAllOption()).displayWhitelistMenu diff --git a/src/common/backup/processor.ts b/src/common/backup/processor.ts index e3111c4f2..07b4c0490 100644 --- a/src/common/backup/processor.ts +++ b/src/common/backup/processor.ts @@ -8,8 +8,9 @@ import BackupDatabase from "@db/backup-database" import metaService from "@service/meta-service" import optionService from "@service/option-service" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import MonthIterator from "@util/month-iterator" +import { judgeVirtualFast } from "@util/pattern" import { formatTime, getBirthday } from "@util/time" import GistCoordinator from "./gist/coordinator" @@ -99,7 +100,7 @@ async function syncFull( // 1. select rows let start = getBirthday() let end = new Date() - const rows = await timerService.select({ date: [start, end] }) + const rows = await statService.select({ date: [start, end] }) const allDates = rows.map(r => r.date).sort((a, b) => a == b ? 0 : a > b ? 1 : -1) client.maxDate = allDates[allDates.length - 1] client.minDate = allDates[0] @@ -199,7 +200,8 @@ class Processor { ...row, cid: id, cname: name, - mergedHosts: [] + mergedHosts: [], + virtual: judgeVirtualFast(row.host), })) })) })) diff --git a/src/content-script/index.ts b/src/content-script/index.ts index 552fa5f25..d155a6489 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -5,51 +5,24 @@ * https://opensource.org/licenses/MIT */ +import { sendMsg2Runtime } from "@api/chrome/runtime" import { initLocale } from "@i18n" import processLimit from "./limit" import printInfo from "./printer" -const host = document.location.host -const url = document.location.href - -function isInWhitelist(host: string): Promise { - const request: timer.mq.Request = { - code: 'cs.isInWhitelist', - data: host - } - return new Promise(resolve => chrome.runtime.sendMessage(request, {}, - (res: timer.mq.Response) => resolve(res.code === 'success' && !!res.data) - )) -} - -function addOneTime(host: string): void { - const request: timer.mq.Request = { - code: 'cs.incVisitCount', - data: host - } - chrome.runtime.sendMessage(request, () => { }) -} - -function printTodayInfo(): Promise { - const request: timer.mq.Request = { - code: 'cs.printTodayInfo', - data: undefined - } - return new Promise(resolve => chrome.runtime.sendMessage(request, - (res: timer.mq.Response) => resolve(res.code === 'success' && !!res.data) - )) -} +const host = document?.location?.host +const url = document?.location?.href async function main() { if (!host) return - const isWhitelist = await isInWhitelist(host) + const isWhitelist = await sendMsg2Runtime('cs.isInWhitelist', host) if (isWhitelist) return - addOneTime(host) + sendMsg2Runtime('cs.incVisitCount', host) await initLocale() - const needPrintInfo = await printTodayInfo() + const needPrintInfo = await sendMsg2Runtime('cs.printTodayInfo') !!needPrintInfo && printInfo(host) processLimit(url) } diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index 282dd9631..e3cbbf091 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -9,26 +9,7 @@ import TimeLimitItem from "@entity/time-limit-item" import optionService from "@service/option-service" import { t2Chrome } from "@i18n/chrome/t" import { t } from "./locale" - -function moreMinutes(url: string): Promise { - const request: timer.mq.Request = { - code: 'cs.moreMinutes', - data: url - } - return new Promise(resolve => chrome.runtime.sendMessage(request, - (res: timer.mq.Response) => resolve(res?.code === 'success' ? res.data || [] : []) - )) -} - -function getLimited(url: string): Promise { - const request: timer.mq.Request = { - code: 'cs.getLimitedRules', - data: url - } - return new Promise(resolve => chrome.runtime.sendMessage(request, - (res: timer.mq.Response) => resolve(res?.code === 'success' ? res.data || [] : []) - )) -} +import { onRuntimeMessage, sendMsg2Runtime } from "@api/chrome/runtime" class _Modal { url: string @@ -69,11 +50,11 @@ class _Modal { const text = t(msg => msg.more5Minutes) link.innerText = text link.onclick = async () => { - const delayRules = await moreMinutes(_thisUrl) + const delayRules: timer.limit.Item[] = await sendMsg2Runtime('cs.moreMinutes', _thisUrl) const wakingRules = delayRules .map(like => TimeLimitItem.of(like)) .filter(rule => !rule.hasLimited()) - chrome.runtime.sendMessage, timer.mq.Response>(wakingMessage(wakingRules)) + sendMsg2Runtime('limitWaking', wakingRules) this.hideModal() } this.delayContainer.append(link) @@ -111,10 +92,6 @@ class _Modal { } } -function wakingMessage(rules: timer.limit.Item[]): timer.mq.Request { - return { code: 'limitWaking', data: rules } -} - const maskStyle: Partial = { width: "100%", height: "100%", @@ -145,10 +122,6 @@ const linkStyle: Partial = { fontSize: '16px !important' } -function openLimitPageMessage(url: string): timer.mq.Request { - return { code: 'openLimitPage', data: encodeURIComponent(url) } -} - function link2Setup(url: string): HTMLParagraphElement { const link = document.createElement('a') Object.assign(link.style, linkStyle) @@ -156,13 +129,13 @@ function link2Setup(url: string): HTMLParagraphElement { const text = t(msg => msg.timeLimitMsg) .replace('{appName}', t2Chrome(msg => msg.meta.name)) link.innerText = text - link.onclick = () => chrome.runtime.sendMessage(openLimitPageMessage(url)) + link.onclick = () => sendMsg2Runtime('openLimitPage', encodeURIComponent(url)) const p = document.createElement('p') p.append(link) return p } -function handleLimitTimeMeet(msg: timer.mq.Request, modal: _Modal): timer.mq.Response { +async function handleLimitTimeMeet(msg: timer.mq.Request, modal: _Modal): Promise { if (msg.code !== "limitTimeMeet") { return { code: "ignore" } } @@ -175,7 +148,7 @@ function handleLimitTimeMeet(msg: timer.mq.Request, modal: _ return { code: "success" } } -function handleLimitWaking(msg: timer.mq.Request, modal: _Modal): timer.mq.Response { +async function handleLimitWaking(msg: timer.mq.Request, modal: _Modal): Promise { if (msg.code !== "limitWaking") { return { code: "ignore" } } @@ -197,7 +170,7 @@ function handleLimitWaking(msg: timer.mq.Request, modal: _Mo return { code: "success" } } -function handleLimitChanged(msg: timer.mq.Request, modal: _Modal): timer.mq.Response { +async function handleLimitChanged(msg: timer.mq.Request, modal: _Modal): Promise { if (msg.code === 'limitChanged') { const data: timer.limit.Item[] = msg.data const items = data.map(TimeLimitItem.of) @@ -210,18 +183,12 @@ function handleLimitChanged(msg: timer.mq.Request, modal: _M export default async function processLimit(url: string) { const modal = new _Modal(url) - const limitedRules: timer.limit.Item[] = await getLimited(url) + const limitedRules: timer.limit.Item[] = await sendMsg2Runtime('cs.getLimitedRules', url) if (limitedRules?.length) { window.onload = () => modal.showModal(!!limitedRules?.filter?.(item => item.allowDelay).length) } - chrome.runtime.onMessage.addListener( - (msg: timer.mq.Request, _sender, sendResponse: timer.mq.Callback) => sendResponse(handleLimitTimeMeet(msg, modal)) - ) - chrome.runtime.onMessage.addListener( - (msg: timer.mq.Request, _sender, sendResponse: timer.mq.Callback) => sendResponse(handleLimitWaking(msg, modal)) - ) - chrome.runtime.onMessage.addListener( - (msg: timer.mq.Request, _sender, sendResponse: timer.mq.Callback) => sendResponse(handleLimitChanged(msg, modal)) - ) + onRuntimeMessage(msg => handleLimitTimeMeet(msg, modal)) + onRuntimeMessage(msg => handleLimitChanged(msg, modal)) + onRuntimeMessage(msg => handleLimitWaking(msg, modal)) } diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 670c6933b..803ea1e02 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -7,23 +7,13 @@ import { t } from "./locale" import { formatPeriod } from "@util/time" - -function getTodayInfo(host: string): Promise { - const request: timer.mq.Request = { - code: 'cs.getTodayInfo', - data: host - } - return new Promise(resolve => chrome.runtime.sendMessage( - request, - (res: timer.mq.Response) => resolve(res?.code === 'success' ? res.data : undefined) - )) -} +import { sendMsg2Runtime } from "@api/chrome/runtime" /** * Print info of today */ export default async function printInfo(host: string) { - const waste: timer.stat.Result = await getTodayInfo(host) + const waste: timer.stat.Result = await sendMsg2Runtime('cs.getTodayInfo', host) const hourMsg = t(msg => msg.timeWithHour) const minuteMsg = t(msg => msg.timeWithMinute) const secondMsg = t(msg => msg.timeWithSecond) diff --git a/src/database/host-alias-database.ts b/src/database/host-alias-database.ts index bec6509d0..17ba063a6 100644 --- a/src/database/host-alias-database.ts +++ b/src/database/host-alias-database.ts @@ -13,20 +13,16 @@ const DB_KEY_PREFIX_M = REMAIN_WORD_PREFIX + "ALIASM" type _AliasSourceAbbr = 'u' | 'd' -const SOURCE_PREFIX_MAP: Record = { - USER: 'u', - DETECTED: 'd' -} const ABBR_MAP: Record<_AliasSourceAbbr, timer.site.AliasSource> = { u: 'USER', d: 'DETECTED' } -function generateKey(aliasKey: timer.site.AliasKey): string { - return (aliasKey.merged ? DB_KEY_PREFIX_M : DB_KEY_PREFIX) + aliasKey.host +function generateKey(SiteKey: timer.site.SiteKey): string { + return (SiteKey.merged ? DB_KEY_PREFIX_M : DB_KEY_PREFIX) + SiteKey.host } -function aliasKeyOf(key: string): timer.site.AliasKey { +function SiteKeyOf(key: string): timer.site.SiteKey { if (key.startsWith(DB_KEY_PREFIX_M)) { return { host: key.substring(DB_KEY_PREFIX_M.length), @@ -40,13 +36,13 @@ function aliasKeyOf(key: string): timer.site.AliasKey { } } -function valueOf(aliasKey: timer.site.AliasKey, value: string): timer.site.Alias { +function valueOf(SiteKey: timer.site.SiteKey, value: string): timer.site.SiteInfo { const abbr = value.substring(0, 1) as _AliasSourceAbbr return { - ...aliasKey, + ...SiteKey, source: ABBR_MAP[abbr], - name: value.substring(1) + alias: value.substring(1) } } @@ -59,36 +55,15 @@ export type HostAliasCondition = { /** * @author zhy * @since 0.5.0 + * @deprecated Use SiteDatabase */ class HostAliasDatabase extends BaseDatabase { - /** - * Update the alias - */ - async update(toUpdate: timer.site.Alias): Promise { - const { name, source } = toUpdate - const key = generateKey(toUpdate) - const value = SOURCE_PREFIX_MAP[source] + name - if (source === 'USER') { - // Force update - return this.storage.put(key, value) - } - const existVal = await this.storage.getOne(key) - if (!existVal || typeof existVal !== 'string') { - // Force update - return this.storage.put(key, value) - } - const abbr = (existVal as string).substring(0, 1) as _AliasSourceAbbr - if (abbr === 'd') { - // Update - return this.storage.put(key, value) - } - } - async selectAll(): Promise { + async selectAll(): Promise { return this.select() } - async select(queryParam?: HostAliasCondition): Promise { + async select(queryParam?: HostAliasCondition): Promise { const host = queryParam?.host const alias = queryParam?.alias const source = queryParam?.source @@ -96,65 +71,25 @@ class HostAliasDatabase extends BaseDatabase { return Object.keys(data) .filter(key => key.startsWith(DB_KEY_PREFIX)) .map(key => { - const aliasKey = aliasKeyOf(key) + const SiteKey = SiteKeyOf(key) const value = data[key] - return valueOf(aliasKey, value) + return valueOf(SiteKey, value) }) .filter(hostAlias => { if (host && !hostAlias.host.includes(host)) return false - if (alias && !hostAlias.name.includes(alias)) return false + if (alias && !hostAlias.alias.includes(alias)) return false if (source && source !== hostAlias.source) return false return true }) } - async get(...hosts: timer.site.AliasKey[]): Promise { - const keys = hosts.map(generateKey) - const items = await this.storage.get(keys) - const result = [] - Object.entries(items).forEach(([key, value]) => { - const aliasKey = aliasKeyOf(key) - result.push(valueOf(aliasKey, value)) - }) - return Promise.resolve(result) - } - - async exist(host: timer.site.AliasKey): Promise { - const key = generateKey(host) - const items = await this.storage.get(key) - return !!items[key] - } - - async existBatch(hosts: timer.site.AliasKey[]): Promise { - const keys = hosts.map(generateKey) - const items = await this.storage.get(keys) - const result: timer.site.AliasKey[] = [] - Object.entries(items).map(([key]) => aliasKeyOf(key)).forEach(host => result.push(host)) - return result - } - - async remove(host: timer.site.AliasKey) { + async remove(host: timer.site.SiteKey) { const key = generateKey(host) await this.storage.remove(key) } async importData(data: any): Promise { - const items = await this.storage.get() - const toSave = {} - Object.entries(data) - .filter(([key, value]) => key.startsWith(DB_KEY_PREFIX) && !!value && typeof value === 'string') - .forEach(([key, value]) => toSave[key] = this.migrate(items[key], value as string)) - await this.storage.set(toSave) - } - - private migrate(exist: string | undefined, toUpdate: string): string { - if (!exist) { - return toUpdate - } - if (exist.startsWith('u') && !toUpdate.startsWith('u')) { - return exist - } - return toUpdate + // Do nothing } } diff --git a/src/database/icon-url-database.ts b/src/database/icon-url-database.ts index 928ff131d..415652286 100644 --- a/src/database/icon-url-database.ts +++ b/src/database/icon-url-database.ts @@ -5,7 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { IS_FIREFOX } from "@util/constant/environment" import BaseDatabase from "./common/base-database" import { REMAIN_WORD_PREFIX } from "./common/constant" @@ -19,41 +18,26 @@ const urlOf = (key: string) => key.substring(DB_KEY_PREFIX.length) * The icon url of hosts * * @since 0.1.7 + * @deprecated Use SiteDatabase */ class IconUrlDatabase extends BaseDatabase { - /** - * Replace or insert - * - * @param host host - * @param iconUrl icon url - */ - put(host: string, iconUrl: string): Promise { - const toUpdate = {} - toUpdate[generateKey(host)] = iconUrl - return this.storage.set(toUpdate) + async listAll(): Promise<{ [host: string]: string }> { + const items = await this.storage.get() + const result = {} + Object.entries(items) + .filter(([key]) => key.startsWith(DB_KEY_PREFIX)) + .forEach(([key, val]) => result[urlOf(key)] = val) + return result } - /** - * @param hosts hosts - */ - async get(...hosts: string[]): Promise<{ [host: string]: string }> { - const keys = hosts.map(generateKey) - const items = await this.storage.get(keys) - const result = {} - Object.entries(items).forEach(([key, iconUrl]) => result[urlOf(key)] = iconUrl) - return Promise.resolve(result) + async remove(host: string): Promise { + const key = generateKey(host) + await this.storage.remove(key) } async importData(data: any): Promise { - const items = await this.storage.get() - const toSave = {} - const chromeEdgeIconUrlReg = /^(chrome|edge):\/\/favicon/ - Object.entries(data) - .filter(([key, value]) => key.startsWith(DB_KEY_PREFIX) && !!value && !items[key]) - .filter(([_key, value]) => !chromeEdgeIconUrlReg.test(value as string)) - .forEach(([key, value]) => toSave[key] = value) - await this.storage.set(toSave) + // Do nothing } } diff --git a/src/database/meta-database.ts b/src/database/meta-database.ts index 11fd1ead5..3c51a1638 100644 --- a/src/database/meta-database.ts +++ b/src/database/meta-database.ts @@ -12,8 +12,8 @@ import { META_KEY } from "./common/constant" * @since 0.6.0 */ class MetaDatabase extends BaseDatabase { - async getMeta(): Promise { - const meta = (await this.storage.getOne(META_KEY)) as timer.meta.ExtensionMeta + async getMeta(): Promise { + const meta = (await this.storage.getOne(META_KEY)) as timer.ExtensionMeta if (!meta) { return {} } else { @@ -22,7 +22,7 @@ class MetaDatabase extends BaseDatabase { } async importData(data: any): Promise { - const meta: timer.meta.ExtensionMeta = data[META_KEY] as timer.meta.ExtensionMeta + const meta: timer.ExtensionMeta = data[META_KEY] as timer.ExtensionMeta if (!meta) { return } @@ -44,7 +44,7 @@ class MetaDatabase extends BaseDatabase { await this.update(existMeta) } - async update(existMeta: timer.meta.ExtensionMeta): Promise { + async update(existMeta: timer.ExtensionMeta): Promise { await this.storage.put(META_KEY, existMeta) } } diff --git a/src/database/site-database.ts b/src/database/site-database.ts new file mode 100644 index 000000000..4f4265c01 --- /dev/null +++ b/src/database/site-database.ts @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import BaseDatabase from "./common/base-database" +import { REMAIN_WORD_PREFIX } from "./common/constant" + +export type SiteCondition = { + host?: string + alias?: string + source?: timer.site.AliasSource + virtual?: boolean +} + +type _Entry = { + /** + * Alias + */ + a?: string + /** + * Auto-detected + */ + d?: boolean + /** + * Icon url + */ + i?: string +} + +const DB_KEY_PREFIX = REMAIN_WORD_PREFIX + 'SITE_' +const HOST_KEY_PREFIX = DB_KEY_PREFIX + 'h' +const VIRTUAL_KEY_PREFIX = DB_KEY_PREFIX + 'v' +const MERGED_FLAG = 'm' + +function cvt2Key({ host, virtual, merged }: timer.site.SiteKey): string { + return virtual + ? VIRTUAL_KEY_PREFIX + host + : HOST_KEY_PREFIX + (merged ? MERGED_FLAG : '_') + host +} + +function cvt2SiteKey(key: string): timer.site.SiteKey { + if (key?.startsWith(VIRTUAL_KEY_PREFIX)) { + return { + host: key.substring(VIRTUAL_KEY_PREFIX.length), + virtual: true, + } + } else if (key?.startsWith(HOST_KEY_PREFIX)) { + return { + host: key.substring(HOST_KEY_PREFIX.length + 1), + merged: key.charAt(HOST_KEY_PREFIX.length) === MERGED_FLAG + } + } +} + +function cvt2Entry({ alias, source, iconUrl }: timer.site.SiteInfo): _Entry { + const entry: _Entry = {} + alias && (entry.a = alias) + source === 'DETECTED' && (entry.d = true) + iconUrl && (entry.i = iconUrl) + return entry +} + +function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry): timer.site.SiteInfo { + if (!entry) return undefined + const { a, d, i } = entry + const siteInfo: timer.site.SiteInfo = { ...key } + siteInfo.alias = a + // Only exist if alias is not empty + a && (siteInfo.source = d ? 'DETECTED' : 'USER') + siteInfo.iconUrl = i + return siteInfo +} + +//////////////////////////////////////////////////////////////////////////// +///////////////////////// ///////////////////////// +///////////////////////// PUBLIC METHODS START ///////////////////////// +///////////////////////// ///////////////////////// +//////////////////////////////////////////////////////////////////////////// + +/** + * Select by condition + * + * @returns list not be undefined, maybe empty + */ +async function select(this: SiteDatabase, condition?: SiteCondition): Promise { + const filter = buildFilter(condition) + const data = await this.storage.get() + return Object.entries(data) + .filter(([key]) => key.startsWith(DB_KEY_PREFIX)) + .map(([key, value]) => cvt2SiteInfo(cvt2SiteKey(key), value as _Entry)) + .filter(filter) +} + +function buildFilter(condition: SiteCondition): (site: timer.site.SiteInfo) => boolean { + const { host, alias, source, virtual } = condition || {} + return site => { + if (host && !site.host.includes(host)) return false + if (alias && !site.alias?.includes(alias)) return false + if (source && source !== site.source) return false + if (virtual !== undefined && virtual !== null) { + const virtualCond = virtual || false + const virtualFactor = site.virtual || false + if (virtualCond !== virtualFactor) return false + } + return true + } +} + +/** + * Get by key + * + * @returns site info, or undefined + */ +async function get(this: SiteDatabase, key: timer.site.SiteKey): Promise { + const entry: _Entry = await this.storage.getOne(cvt2Key(key)) + if (!entry) { + return undefined + } + return cvt2SiteInfo(key, entry) +} + +async function getBatch(this: SiteDatabase, keys: timer.site.SiteKey[]): Promise { + const result = await this.storage.get(keys.map(cvt2Key)) + return Object.entries(result) + .map(([key, value]) => cvt2SiteInfo(cvt2SiteKey(key), value as _Entry)) +} + +/** + * Save site info + */ +async function save(this: SiteDatabase, siteInfo: timer.site.SiteInfo): Promise { + this.storage.put(cvt2Key(siteInfo), cvt2Entry(siteInfo)) +} + +async function remove(this: SiteDatabase, siteKey: timer.site.SiteKey): Promise { + this.storage.remove(cvt2Key(siteKey)) +} + +async function exist(this: SiteDatabase, siteKey: timer.site.SiteKey): Promise { + const key = cvt2Key(siteKey) + const entry: _Entry = await this.storage.getOne(key) + return !!entry +} + +async function existBatch(this: SiteDatabase, siteKeys: timer.site.SiteKey[]): Promise { + const keys = siteKeys.map(cvt2Key) + const items = await this.storage.get(keys) + return Object.entries(items).map(([key]) => cvt2SiteKey(key)) +} + +async function importData(this: SiteDatabase, data: any) { + throw new Error("Method not implemented.") +} + +//////////////////////////////////////////////////////////////////////////// +///////////////////////// ///////////////////////// +///////////////////////// PUBLIC METHODS END ///////////////////////// +///////////////////////// ///////////////////////// +//////////////////////////////////////////////////////////////////////////// + +class SiteDatabase extends BaseDatabase { + select = select + get = get + getBatch = getBatch + save = save + remove = remove + exist = exist + existBatch = existBatch + + importData = importData + + /** + * Add listener to listen changes + * + * @since 1.6.0 + */ + addChangeListener(listener: (oldAndNew: [timer.site.SiteInfo, timer.site.SiteInfo][]) => void) { + const storageListener = ( + changes: { [key: string]: chrome.storage.StorageChange }, + _areaName: "sync" | "local" | "managed" + ) => { + const changedSites: [timer.site.SiteInfo, timer.site.SiteInfo][] = Object.entries(changes) + .filter(([k]) => k.startsWith(DB_KEY_PREFIX)) + .map(([k, v]) => { + const siteKey = cvt2SiteKey(k) + const oldVal = cvt2SiteInfo(siteKey, v?.oldValue as _Entry) + const newVal = cvt2SiteInfo(siteKey, v?.newValue as _Entry) + return [oldVal, newVal] + }) + changedSites.length && listener?.(changedSites) + } + chrome.storage.onChanged.addListener(storageListener) + } +} + +export default SiteDatabase \ No newline at end of file diff --git a/src/database/timer-database.ts b/src/database/stat-database.ts similarity index 88% rename from src/database/timer-database.ts rename to src/database/stat-database.ts index bead4cfdb..6ba2fb632 100644 --- a/src/database/timer-database.ts +++ b/src/database/stat-database.ts @@ -10,8 +10,9 @@ import { formatTime } from "@util/time" import BaseDatabase from "./common/base-database" import { DATE_FORMAT, REMAIN_WORD_PREFIX } from "./common/constant" import { createZeroResult, mergeResult, isNotZeroResult } from "@util/stat" +import { judgeVirtualFast } from "@util/pattern" -export type TimerCondition = { +export type StatCondition = { /** * Date * {y}{m}{d} @@ -41,7 +42,7 @@ export type TimerCondition = { fullHost?: boolean } -type _TimerCondition = TimerCondition & { +type _StatCondition = StatCondition & { // Use exact date condition useExactDate?: boolean // date str @@ -55,7 +56,7 @@ type _TimerCondition = TimerCondition & { focusEnd?: number } -function processDateCondition(cond: _TimerCondition, paramDate: Date | Date[]) { +function processDateCondition(cond: _StatCondition, paramDate: Date | Date[]) { if (!paramDate) return if (paramDate instanceof Date) { @@ -73,20 +74,20 @@ function processDateCondition(cond: _TimerCondition, paramDate: Date | Date[]) { } } -function processParamTimeCondition(cond: _TimerCondition, paramTime: number[]) { +function processParamTimeCondition(cond: _StatCondition, paramTime: number[]) { if (!paramTime) return paramTime.length >= 2 && (cond.timeEnd = paramTime[1]) paramTime.length >= 1 && (cond.timeStart = paramTime[0]) } -function processParamFocusCondition(cond: _TimerCondition, paramFocus: number[]) { +function processParamFocusCondition(cond: _StatCondition, paramFocus: number[]) { if (!paramFocus) return paramFocus.length >= 2 && (cond.focusEnd = paramFocus[1]) paramFocus.length >= 1 && (cond.focusStart = paramFocus[0]) } -function processCondition(condition: TimerCondition): _TimerCondition { - const result: _TimerCondition = { ...condition } +function processCondition(condition: StatCondition): _StatCondition { + const result: _StatCondition = { ...condition } processDateCondition(result, condition.date) processParamTimeCondition(result, condition.timeRange) processParamFocusCondition(result, condition.focusRange) @@ -122,20 +123,15 @@ function migrate(exists: { [key: string]: timer.stat.Result }, data: any): { [ke return result } -class TimerDatabase extends BaseDatabase { +class StatDatabase extends BaseDatabase { async refresh(): Promise<{}> { const result = await this.storage.get(null) const items = {} Object.entries(result) - .filter(([key]) => - !key.startsWith(REMAIN_WORD_PREFIX) - // The prefix of archived data, historical issues - // todo: delete this line - && !key.startsWith('_a_') - ) + .filter(([key]) => !key.startsWith(REMAIN_WORD_PREFIX)) .forEach(([key, value]) => items[key] = value) - return Promise.resolve(items) + return items } /** @@ -184,10 +180,10 @@ class TimerDatabase extends BaseDatabase { * * @param condition condition */ - async select(condition?: TimerCondition): Promise { + async select(condition?: StatCondition): Promise { log("select:{condition}", condition) condition = condition || {} - const _cond: _TimerCondition = processCondition(condition) + const _cond: _StatCondition = processCondition(condition) const items = await this.refresh() let result: timer.stat.Row[] = [] @@ -197,7 +193,7 @@ class TimerDatabase extends BaseDatabase { const val: timer.stat.Result = items[key] if (this.filterBefore(date, host, val, _cond)) { const { focus, time } = val - result.push({ date, host, focus, time, mergedHosts: [] }) + result.push({ date, host, focus, time, mergedHosts: [], virtual: judgeVirtualFast(host) }) } } @@ -205,7 +201,7 @@ class TimerDatabase extends BaseDatabase { return result } - private filterHost(host: string, condition: _TimerCondition): boolean { + private filterHost(host: string, condition: _StatCondition): boolean { const paramHost = (condition.host || '').trim() if (!paramHost) return true if (!!condition.fullHost && host !== paramHost) return false @@ -213,7 +209,7 @@ class TimerDatabase extends BaseDatabase { return true } - private filterDate(date: string, condition: _TimerCondition): boolean { + private filterDate(date: string, condition: _StatCondition): boolean { if (condition.useExactDate) { if (condition.exactDateStr !== date) return false } else { @@ -241,7 +237,7 @@ class TimerDatabase extends BaseDatabase { * @param condition query parameters * @return true if valid, or false */ - private filterBefore(date: string, host: string, val: timer.stat.Result, condition: _TimerCondition): boolean { + private filterBefore(date: string, host: string, val: timer.stat.Result, condition: _StatCondition): boolean { const { focus, time } = val const { timeStart, timeEnd, focusStart, focusEnd } = condition @@ -334,9 +330,9 @@ class TimerDatabase extends BaseDatabase { * @returns count * @since 1.0.2 */ - async count(condition: TimerCondition): Promise { + async count(condition: StatCondition): Promise { condition = condition || {} - const _cond: _TimerCondition = processCondition(condition) + const _cond: _StatCondition = processCondition(condition) const items = await this.refresh() let count = 0 @@ -359,4 +355,4 @@ class TimerDatabase extends BaseDatabase { } } -export default TimerDatabase +export default StatDatabase diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts index 23db0df4c..33e4de794 100644 --- a/src/guide/component/common.ts +++ b/src/guide/component/common.ts @@ -4,6 +4,7 @@ import type { VNode } from "vue" import { t, tN } from "@guide/locale" import { h } from "vue" import { position2AnchorClz } from "@guide/util" +import { createTab } from "@api/chrome/tab" export function h1(i18nKey: I18nKey, position: Position, i18nParam?: any): VNode { return h('h1', { class: `guide-h1 ${position2AnchorClz(position)}` }, t(i18nKey, i18nParam)) @@ -24,7 +25,7 @@ export function link(href: string, text: string): VNode { export function linkInner(extensionUrl: string, text: string): VNode { return h('a', { class: 'guide-link', - onClick: () => chrome.tabs.create({ url: extensionUrl }), + onClick: () => createTab(extensionUrl), }, text) } diff --git a/src/guide/layout/content.ts b/src/guide/layout/content.ts index 5a9e0c944..91217a2f1 100644 --- a/src/guide/layout/content.ts +++ b/src/guide/layout/content.ts @@ -7,7 +7,6 @@ import Privacy from "../component/privacy" import { position2AnchorClz } from "@guide/util" function scrollPosition(position: Position) { - console.log(position) document.querySelector(`.${position2AnchorClz(position)}`)?.scrollIntoView?.() } diff --git a/src/i18n/i18n.d.ts b/src/i18n/i18n.d.ts index ffa025784..cd2e6cbd6 100644 --- a/src/i18n/i18n.d.ts +++ b/src/i18n/i18n.d.ts @@ -1,4 +1,9 @@ +type RequiredMessages = { + [locale in timer.RequiredLocale]: M +} -type Messages = { - [locale in timer.Locale]: M +type OptionalMessages = { + [locale in timer.OptionalLocale]?: EmbeddedPartial } + +type Messages = RequiredMessages & OptionalMessages diff --git a/src/i18n/index.ts b/src/i18n/index.ts index e724ffcfb..d23bf7190 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -109,12 +109,24 @@ export async function initLocale() { optionService.addOptionChangeListener(handleLocaleOption) +function tryGetOriginalI18nVal( + messages: Messages, + keyPath: I18nKey, + specLocale?: timer.Locale +) { + try { + return keyPath(messages[specLocale || locale]) + } catch (ignore) { + return undefined + } +} + export function getI18nVal( messages: Messages, keyPath: I18nKey, specLocale?: timer.Locale ): string { - const result = keyPath(messages[specLocale || locale]) + const result = tryGetOriginalI18nVal(messages, keyPath, specLocale) || keyPath(messages[FEEDBACK_LOCALE]) || '' return typeof result === 'string' ? result : JSON.stringify(result) @@ -141,4 +153,4 @@ export function t(messages: Messages, props: Translate return param ? fillWithParam(result, param) : result } -export type I18nKey = (messages: MessageType) => any +export type I18nKey = (messages: MessageType | EmbeddedPartial) => any diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index 4ea16502e..d6e6cc564 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -25,7 +25,7 @@ const _default: Messages = { l1: '由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。', l2: '为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。', l3: '如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。', - l4: '当某种语言的翻译进度达到 80% 之后,我将会考虑在扩展中支持它。', + l4: '当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。', }, button: '前往 Crowdin', loading: '正在查询翻译进度...', @@ -33,50 +33,17 @@ const _default: Messages = { en: { title: 'Feel free to help improve the extension\'s localization translations!', alert: { - l1: 'Due to the author\'s language ability, \ - the extension only supports Simplified Chinese and English natively, \ - and other languages are either missing or rely heavily on machine translation.', - l2: 'In order to provide a better user experience, \ - I host the translation tasks for other languages on Crowdin. \ - Crowdin is a translation management system free for open source software.', - l3: 'If you find this extension useful to you and you are willing to improve its translation, \ - you can click the button below to go to the project home page on Crowdin.', - l4: 'When the translation progress of a language reaches 80%, I will consider supporting it in this extension.', - }, - button: 'Go Crowdin', - loading: 'Checking translation progress...', - }, - ja: { - title: 'Feel free to help improve the extension\'s localization translations!', - alert: { - l1: 'Due to the author\'s language ability, \ - the extension only supports Simplified Chinese and English natively, \ - and other languages are either missing or rely heavily on machine translation.', - l2: 'In order to provide a better user experience, \ - I host the translation tasks for other languages on Crowdin. \ - Crowdin is a translation management system free for open source software.', - l3: 'If you find this extension useful to you and you are willing to improve its translation, \ - you can click the button below to go to the project home page on Crowdin.', - l4: 'When the translation progress of a language reaches 80%, I will consider supporting it in this extension.', + l1: 'Due to the author\'s language ability, the extension only supports Simplified Chinese and English natively, and other languages are either missing or rely heavily on machine translation.', + l2: 'In order to provide a better user experience, I host the translation tasks for other languages on Crowdin.Crowdin is a translation management system free for open source software.', + l3: 'If you find this extension useful to you and you are willing to improve its translation,you can click the button below to go to the project home page on Crowdin.', + l4: 'When the translation progress of a language reaches 50%, I will consider supporting it in this extension.', }, button: 'Go Crowdin', loading: 'Checking translation progress...', }, zh_TW: { - title: 'Feel free to help improve the extension\'s localization translations!', - alert: { - l1: 'Due to the author\'s language ability, \ - the extension only supports Simplified Chinese and English natively, \ - and other languages are either missing or rely heavily on machine translation.', - l2: 'In order to provide a better user experience, \ - I host the translation tasks for other languages on Crowdin. \ - Crowdin is a translation management system free for open source software.', - l3: 'If you find this extension useful to you and you are willing to improve its translation, \ - you can click the button below to go to the project home page on Crowdin.', - l4: 'When the translation progress of a language reaches 80%, I will consider supporting it in this extension.', - }, - button: 'Go Crowdin', - loading: 'Checking translation progress...', + button: '前往 Crowdin', + loading: '正在檢查翻譯進度...', }, } diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index ccf04f374..e5b3847cf 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -34,7 +34,7 @@ const _default: Messages = { dataHistory: '历史趋势', dataClear: '内存管理', additional: '附加功能', - siteManage: '网站名称管理', + siteManage: '网站管理', whitelist: '白名单管理', mergeRule: '子域名合并', option: '扩展选项', @@ -54,7 +54,7 @@ const _default: Messages = { dataHistory: '曆史趨勢', dataClear: '內存管理', additional: '附加功能', - siteManage: '網站名稱管理', + siteManage: '網站管理', whitelist: '白名單管理', mergeRule: '子域名合並', option: '擴充選項', @@ -104,9 +104,9 @@ const _default: Messages = { option: '拡張設定', feedback: 'フィードバックアンケート', rate: 'それを評価', - helpUs: 'Help Us', + helpUs: '協力する', userManual: 'ユーザーマニュアル', - } + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/merge-rule.ts b/src/i18n/message/app/merge-rule.ts index 2b2ae37b2..87a91092f 100644 --- a/src/i18n/message/app/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -54,7 +54,7 @@ const _default: Messages = { infoAlert0: '點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則', infoAlert1: '原網域可填具體的網域或者正則表達式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則', infoAlert2: '合並後網域可填具體的網域,或者填數字,或者不填', - infoAlert3: '如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn', + infoAlert3: '如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn', infoAlert4: '如果不填,則表示原網域不會被合並', infoAlert5: '如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級', }, @@ -88,7 +88,7 @@ const _default: Messages = { infoAlert0: '[追加] ボタンをクリックすると、元のドメイン名と結合されたドメイン名の入力ボックスがポップアップし、ルールを入力して保存します。', infoAlert1: '元のドメイン名には、特定のドメイン名または正規表現 (www.baidu.com、*.baidu.com、*.google.com.* など) を入力できます。 マージ時にこのルールを使用するドメインを決定するには', infoAlert2: '統合されたドメイン名の後、特定のドメイン名を入力するか、番号を入力するか、空白のままにすることができます', - infoAlert3: '数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。', + infoAlert3: '数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。', infoAlert4: '記入しない場合は、元のドメイン名が統合されないことを意味します', infoAlert5: '一致するルールがない場合、デフォルトで {psl} より前のレベルになります', }, diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 9035cef47..b71ec76ba 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -171,7 +171,7 @@ const _default: Messages = { auto: { label: '是否开启自动备份', interval: '每 {input} 分钟备份一次', - } + }, }, resetButton: '恢复默认', resetSuccess: '成功重置为默认值', @@ -256,7 +256,7 @@ const _default: Messages = { auto: { label: '是否開啟自動備份', interval: '每 {input} 分鐘備份一次', - } + }, }, resetButton: '恢複默認', resetSuccess: '成功重置爲默認值', @@ -355,7 +355,7 @@ const _default: Messages = { max: '最初の {input} 個のデータのみを表示し、残りのエントリは結合されます', defaultMergeDomain: '{input} オープン時にサブドメインをマージ', defaultDisplay: '開くと {duration} {type} が表示されます', - displaySiteName: '{input} ホストの代わりに {siteName} {siteNameUsage} を表示するかどうか', + displaySiteName: '{input} ホストの代わりに {siteName} を表示するかどうか', durationWidth: '100px', weekStart: '週の最初の日 {input}', weekStartAsNormal: 'いつものように', @@ -417,7 +417,7 @@ const _default: Messages = { gist: { label: 'Github Gist', auth: 'Personal Access Token {info} {input}', - authInfo: 'One token with at least gist permission is required', + authInfo: '少なくとも gist 権限を持つトークンが 1 つ必要です', }, }, alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', diff --git a/src/i18n/message/app/report.ts b/src/i18n/message/app/report.ts index dccc20432..cb8d79cfa 100644 --- a/src/i18n/message/app/report.ts +++ b/src/i18n/message/app/report.ts @@ -68,7 +68,7 @@ const _default: Messages = { localData: '本地数据', value: '对应数值', percentage: '百分比', - } + }, }, }, zh_TW: { @@ -100,7 +100,7 @@ const _default: Messages = { localData: '本地數據', value: '對應數值', percentage: '百分比', - } + }, }, }, en: { @@ -132,7 +132,7 @@ const _default: Messages = { localData: 'Local Data', value: 'Value', percentage: 'Percentage', - } + }, }, }, ja: { diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts index b3c46d264..d2d1266f0 100644 --- a/src/i18n/message/app/site-manage.ts +++ b/src/i18n/message/app/site-manage.ts @@ -12,9 +12,16 @@ export type SiteManageMessage = { deleteConfirmMsg: string column: { host: string + type: string alias: string aliasInfo: string source: string + icon: string + } + type: { + normal: string + merged: string + virtual: string } source: { user: string @@ -23,7 +30,6 @@ export type SiteManageMessage = { button: { add: string delete: string - modify: string save: string } form: { @@ -35,6 +41,7 @@ export type SiteManageMessage = { saved: string existedTag: string mergedTag: string + virtualTag: string } } @@ -46,9 +53,16 @@ const _default: Messages = { deleteConfirmMsg: '{host} 的名称设置将会被删除', column: { host: '网站域名', + type: '网站类型', alias: '网站名称', aliasInfo: '网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名', - source: '来源', + source: '名称来源', + icon: '网站图标', + }, + type: { + normal: '普通站点', + merged: '合并站点', + virtual: '自定义站点', }, source: { user: '手动设置', @@ -57,7 +71,6 @@ const _default: Messages = { button: { add: '新增', delete: '删除', - modify: '修改', save: '保存', }, form: { @@ -69,6 +82,7 @@ const _default: Messages = { saved: '已保存', existedTag: '已存在', mergedTag: '合并', + virtualTag: '自定义', }, }, zh_TW: { @@ -78,18 +92,24 @@ const _default: Messages = { deleteConfirmMsg: '{host} 的名稱設置將會被刪除', column: { host: '網站域名', + type: '網站類型', alias: '網站名稱', aliasInfo: '網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域', - source: '來源', + source: '名稱來源', + icon: '網站圖標', }, source: { user: '手動設置', detected: '自動抓取', }, + type: { + normal: '普通站點', + merged: '合併站點', + virtual: '自定義站點', + }, button: { add: '新增', delete: '刪除', - modify: '修改', save: '保存', }, form: { @@ -110,9 +130,16 @@ const _default: Messages = { deleteConfirmMsg: 'The name of {host} will be deleted', column: { host: 'Site URL', + type: 'Site Type', alias: 'Site Name', aliasInfo: 'The site name will be shown on the record page and the popup page', - source: 'Source', + source: 'Name Source', + icon: 'Icon', + }, + type: { + normal: 'normal', + merged: 'merged', + virtual: 'virtual', }, source: { user: 'user-maintained', @@ -121,7 +148,6 @@ const _default: Messages = { button: { add: 'New', delete: 'Delete', - modify: 'Modify', save: 'Save', }, form: { @@ -133,6 +159,7 @@ const _default: Messages = { saved: 'Saved', existedTag: 'EXISTED', mergedTag: 'MERGED', + virtualTag: 'VIRTUAL', }, }, ja: { @@ -153,7 +180,6 @@ const _default: Messages = { button: { add: '追加', delete: '削除', - modify: '変更', save: '保存', }, form: { diff --git a/src/i18n/message/app/trend.ts b/src/i18n/message/app/trend.ts index 6ceb167e4..5aa9b77d7 100644 --- a/src/i18n/message/app/trend.ts +++ b/src/i18n/message/app/trend.ts @@ -21,6 +21,7 @@ export type TrendMessage = { saveAsImageTitle: string defaultSubTitle: string merged: string + virtual: string } const _default: Messages = { @@ -45,6 +46,7 @@ const _default: Messages = { saveAsImageTitle: '保存', defaultSubTitle: '请先在左上角选择需要分析的域名', merged: '合并', + virtual: '自定义', }, zh_TW: { hostPlaceholder: '蒐索你想分析的網域', @@ -67,6 +69,7 @@ const _default: Messages = { saveAsImageTitle: '保存', defaultSubTitle: '請先在左上角選擇需要分析的網域', merged: '合並', + virtual: '自定義', }, en: { hostPlaceholder: 'Search site URL', @@ -89,6 +92,7 @@ const _default: Messages = { saveAsImageTitle: 'Snapshot', defaultSubTitle: 'Search and select one URL to analyze on the top-left corner, pls', merged: 'Merged', + virtual: 'Virtual', }, ja: { hostPlaceholder: 'ドメイン名を検索', @@ -111,6 +115,7 @@ const _default: Messages = { saveAsImageTitle: 'ダウンロード', defaultSubTitle: 'まず、左上隅で分析するドメイン名を選択します', merged: '合并', + virtual: 'カスタマイズ', }, } diff --git a/src/i18n/message/common/context-menus.ts b/src/i18n/message/common/context-menus.ts index b2187a2b8..09117dee4 100644 --- a/src/i18n/message/common/context-menus.ts +++ b/src/i18n/message/common/context-menus.ts @@ -39,8 +39,8 @@ const _default: Messages = { feedbackPage: 'Issues', }, ja: { - add2Whitelist: 'ホワイトリスト', - removeFromWhitelist: 'ホワイトリストから削除する', + add2Whitelist: 'ホワイトリストに {host} を追加', + removeFromWhitelist: 'ホワイトリストから {host} を削除します', optionPage: '拡張設定', repoPage: 'ソースコード', feedbackPage: 'フィードバックの欠如', diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 66f7982c4..4be2cc2ac 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -83,7 +83,7 @@ const _default: Messages = { quickstart: 'クイックスタート', background: 'すべての機能', advanced: '高度な機能', - backup: 'Backup your data with Gist', + backup: 'Gist でデータをバックアップ', }, privacy: { title: 'ポリシーと規約', diff --git a/src/i18n/message/guide/privacy.ts b/src/i18n/message/guide/privacy.ts index 4517bfd8f..7e8401c28 100644 --- a/src/i18n/message/guide/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -58,7 +58,7 @@ const _default: Messages = { }, storage: { p1: 'We guarantee that all data collected by this extension will only be saved in your browser\'s local storage and will never be distributed elsewhere.', - p2: 'You can however use the tools provided by the extension to export or import your data in JSON or CSV file format. The extension also supports you to use GitHub Gist, etc., third-party services you trust enough to back up your data.', + p2: 'You can however use the tools provided by the extension to export or import your data in JSON or CSV file format. The extension also supports you to use GitHub Gist, etc., third-party services you trust enough to back up your data.', p3: 'We only help you collect data, but the right of disposal must be yours.', }, }, @@ -71,7 +71,7 @@ const _default: Messages = { }, storage: { p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', - p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', + p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', }, }, diff --git a/src/i18n/message/guide/profile.ts b/src/i18n/message/guide/profile.ts index e0d02b024..afa6c876a 100644 --- a/src/i18n/message/guide/profile.ts +++ b/src/i18n/message/guide/profile.ts @@ -23,7 +23,7 @@ const _default: Messages = { p2: '這個頁面將會告訴您如何使用它,以及相關的隱私政策。', }, en: { - p1: '{appName} is a browser extension to track the time you spent on all websites. You can check out its source code on {github}.', + p1: '{appName} is a browser extension to track the time you spent on all websites.You can check out its source code on {github}.', p2: 'This page will tell you how to use it, and the related privacy policy.', }, ja: { diff --git a/src/i18n/message/guide/usage.ts b/src/i18n/message/guide/usage.ts index 4b048c8c5..2026a441b 100644 --- a/src/i18n/message/guide/usage.ts +++ b/src/i18n/message/guide/usage.ts @@ -74,7 +74,7 @@ const _default: Messages = { p1: '您可以按以下步骤使用 {gist} 备份您的数据。之后,您可在其他终端上查询已备份数据。', l1: '1. 首先,您需要在 Github 生成一个包含 gist 权限的 {token}。', l2: '2. 然后在选项页面将同步方式选为 Github Gist,将你的 token 填入下方出现的输入框中。', - l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。' + l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。', }, }, zh_TW: { @@ -107,28 +107,28 @@ const _default: Messages = { p1: '您可以按以下步驟使用 {gist} 備份您的數據。之後,您可在其他終端上查詢已備份數據。', l1: '1. 首先,您需要在 Github 生成一個包含 gist 權限的 {token}。', l2: '2. 然後在選項頁面將同步方式選為 Github Gist,將你的 token 填入下方出現的輸入框中。', - l3: '3. 最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。' + l3: '3. 最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。', }, }, en: { quickstart: { p1: 'First, you can quickly start using this extension by following these steps:', - l1: '1. Pin the icon of this extension in the upper right corner of the browser. The specific operation method depends on your browser. This step will not affect the normal behavior of it, but will greatly improve your interactive experience.', - l2: '2. Visit any website and browse for a few seconds, then you will observe a number jumping on the icon. it shows how much time you spent today browsing current website', + l1: '1. Pin the icon of this extension in the upper right corner of the browser. The specific operation method depends on your browser.This step will not affect the normal behavior of it, but will greatly improve your interactive experience.', + l2: '2. Visit any website and browse for a few seconds, then you will observe a number jumping on the icon.it shows how much time you spent today browsing current website', l3: '3. Click the icon, and a page will pop up, showing your stat data for today or recent days.', - p2: 'It is worth mentioning that since the duration data can only be counted in real time, the history before installation will not be recorded.', + p2: 'It is worth mentioning that since the duration data can only be counted in real time,the history before installation will not be recorded.', }, background: { - p1: 'Based on icons, the extension provides a more convenient way to view data. But if you want to experience its full functionality, you need to visit {background} of the extension. There are two ways to enter the background page:', + p1: 'Based on icons, the extension provides a more convenient way to view data.But if you want to experience its full functionality, you need to visit {background} of the extension.There are two ways to enter the background page:', l1: '1. You can right-click the icon of the extension, and click [{allFunction}] in the pop-up menu.', l2: '2. You can also find the [{allFunction}] link at the bottom of the icon popup page, just click it.', p2: 'The popup page and background page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', backgroundPage: 'the background page', }, advanced: { - p1: 'The core function of this extension is to count your browsing behavior on different websites. In addition, it also provides many advanced functions to meet your more needs. Of course, you can find all the functions in the background page.', + p1: 'The core function of this extension is to count your browsing behavior on different websites.In addition, it also provides many advanced functions to meet your more needs.Of course, you can find all the functions in the background page.', l1: '1. It can analyze the trend of your visiting the same website over a period of time, and display it in a line chart.', - l2: '2. It can count your surfing frequency in different time periods every day, and display it in a histogram. The data is site-agnostic and has a minimum statistical granularity of 15 minutes.', + l2: '2. It can count your surfing frequency in different time periods every day, and display it in a histogram.The data is site-agnostic and has a minimum statistical granularity of 15 minutes.', l3: '3. It can count the time you read local files, but this function needs to be enabled in the options.', l4: '4. It supports the whitelist function, you can add the websites you don\'t want to count to the whitelist.', l5: '5. It supports merging statistics of several related websites into the same entry, and you can customize the rules for merging. Merge by {psl} by default.', @@ -137,31 +137,29 @@ const _default: Messages = { l8: '8. It supports using Github Gist as the cloud to store data of multiple browsers and perform aggregated queries. You need to prepare a token with at least gist permission.', }, backup: { - p1: 'You can use {gist} to backup your data by following the steps below. \ - Afterwards, you can query the backed up data on other terminals.', + p1: 'You can use {gist} to backup your data by following the steps below.Afterwards, you can query the backed up data on other terminals.', l1: '1. First, you need to generate a {token} with gist permissions on Github.', - l2: '2. Then select Github Gist as the synchronization method on the options page, \ - and fill in your token in the input box that appears below.', - l3: '3. Finally, click the backup button to import the local data into your gist.' + l2: '2. Then select Github Gist as the synchronization method on the options page,and fill in your token in the input box that appears below.', + l3: '3. Finally, click the backup button to import the local data into your gist.', }, }, ja: { quickstart: { p1: 'まず、次の手順に従って、この拡張機能の調査を開始できます。', - l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', - l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。 これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', + l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', + l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', l3: '3. 拡張機能のアイコンをクリックすると、ページがポップアップし、今日または最近のインターネット データが表示されます。', p2: 'なお、継続時間データはリアルタイムでカウントされるため、拡張機能をインストールする前の閲覧履歴は記録されません。', }, background: { - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', + p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [{allFunction}] をクリックします。', l2: '2. また、アイコン ポップアップ ページの下部に [{allFunction}] リンクがあり、それをクリックするだけです。', p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', backgroundPage: '背景ページ', }, advanced: { - p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', + p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', l1: '1. 一定期間の同じ Web サイトへのアクセスの傾向を分析し、折れ線グラフで表示できます。', l2: '2. あなたのネットサーフィン頻度を毎日異なる時間帯でカウントし、ヒストグラムで表示できます。 データはサイトにとらわれず、最小の統計粒度は 15 分です。', l3: '3. ローカル ファイルの読み取り時間をカウントできますが、この機能はオプションで有効にする必要があります。', @@ -169,13 +167,13 @@ const _default: Messages = { l5: '5. 複数の関連 Web サイトの統計を同じエントリにマージすることをサポートし、マージのルールをカスタマイズできます。 デフォルトでは {psl} でマージします。', l6: '6. 各 Web サイトの毎日の閲覧時間の制限をサポートしています。これには、制限ルールを手動で追加する必要があります。', l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', - l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', + l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', }, backup: { p1: '以下の手順に従って、{gist} を使用してデータをバックアップできます。その後、バックアップされたデータを他の端末で照会できます。', l1: '1. まず、Github で Gist 権限を持つ {token} を生成する必要があります。', l2: '2. 次に、オプション ページで同期方法として [Github Gist] を選択し、下に表示される入力ボックスにトークンを入力します。', - l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。' + l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。', }, }, } diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index 71a55a4a4..ec117a37d 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -28,7 +28,7 @@ const _default: Messages = { today: '今日数据', thisWeek: '本周数据', thisMonth: '本月数据', - last30Days: '近 30 天数据' + last30Days: '近 30 天数据', }, mergeHostLabel: '合并子域名', fileName: '上网时长清单_{today}_by_{app}', diff --git a/src/package.ts b/src/package.ts index 8fcfceb90..c442574d7 100644 --- a/src/package.ts +++ b/src/package.ts @@ -5,18 +5,13 @@ * https://opensource.org/licenses/MIT */ -import packageJson from '../package.json' +import packageJson from "../package.json" +type _AllPackageInfo = typeof packageJson // The declaration of package.json -type _PackageJson = { - name: string - description: string - version: string - homepage: string - author: string -} +type _PackageInfo = Pick<_AllPackageInfo, 'name' | 'description' | 'version' | 'homepage' | 'author'> -const _default: _PackageJson = { +const _default: _PackageInfo = { name: packageJson.name, description: packageJson.description, version: packageJson.version, diff --git a/src/popup/components/chart/click-handler.ts b/src/popup/components/chart/click-handler.ts index fb5980990..3dd0fb094 100644 --- a/src/popup/components/chart/click-handler.ts +++ b/src/popup/components/chart/click-handler.ts @@ -9,6 +9,7 @@ import type { CallbackDataParams } from "echarts/types/dist/shared" import { REPORT_ROUTE } from "@app/router/constants" import { getAppPageUrl } from "@util/constant/url" +import { createTab } from "@api/chrome/tab" function generateUrl(data: PopupRow, queryResult: PopupQueryResult): string { const { host, isOther } = data @@ -45,7 +46,7 @@ function handleClick(params: CallbackDataParams, queryResult: PopupQueryResult) const componentType = params.componentType if (componentType === 'series') { const url = generateUrl(data, queryResult) - url && chrome.tabs.create({ url }) + url && createTab(url) } } diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index 7d2fdfe8c..58af031b9 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -12,6 +12,7 @@ import type { ToolboxComponentOption, TooltipComponentOption, LegendComponentOption, + } from "echarts/components" import { formatPeriodCommon, formatTime } from "@util/time" @@ -22,6 +23,7 @@ import { IS_SAFARI } from "@util/constant/environment" import { OPTION_ROUTE } from "@app/router/constants" import { getAppPageUrl } from "@util/constant/url" import { optionIcon } from "./toolbox-icon" +import { createTab } from "@api/chrome/tab" type EcOption = ComposeOption< | PieSeriesOption @@ -31,17 +33,29 @@ type EcOption = ComposeOption< | LegendComponentOption > +// The declarations of labels +type PieLabelRichOption = PieSeriesOption['label']['rich'] +type PieLabelRichValueOption = PieLabelRichOption[string] +// The declaration of data item +type PieSeriesItemOption = PieSeriesOption['data'][0] & { + host: string, + iconUrl?: string, + isOther?: boolean +} + type ChartProps = PopupQueryResult & { displaySiteName: boolean } const today = formatTime(new Date(), '{y}_{m}_{d}') -/** - * If the percentage of target site is less than SHOW_ICON_THRESHOLD, don't show its icon - */ const LABEL_FONT_SIZE = 13 const LABEL_ICON_SIZE = 13 +const BASE_LABEL_RICH_VALUE: PieLabelRichValueOption = { + height: LABEL_ICON_SIZE, + width: LABEL_ICON_SIZE, + fontSize: LABEL_FONT_SIZE, +} const legend2LabelStyle = (legend: string) => { const code = [] @@ -82,9 +96,10 @@ function toolTipFormatter({ type, dateLength }: PopupQueryResult, params: any): function labelFormatter({ mergeHost }: PopupQueryResult, params: any): string { const format = params instanceof Array ? params[0] : params const { name } = format - const data = format.data as PopupRow + const data = format.data as PieSeriesItemOption + const { isOther, iconUrl } = data // Un-supported to get favicon url in Safari - return mergeHost || data.isOther || IS_SAFARI + return mergeHost || isOther || !iconUrl || IS_SAFARI ? name : `{${legend2LabelStyle(name)}|} {a|${name}}` } @@ -208,25 +223,20 @@ export function pieOptions(props: ChartProps, container: HTMLDivElement): EcOpti show: true, title: t(msg => msg.chart.options), icon: optionIcon, - onclick() { - chrome.tabs.create({ url: getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' }) }) - } + onclick: () => createTab(getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' })) } } } } - const series = [] - const iconRich = {} + const series: PieSeriesItemOption[] = [] + const iconRich: PieLabelRichOption = {} data.forEach(d => { - const { host, alias, isOther } = d + const { host, alias, isOther, iconUrl } = d const legend = displaySiteName ? (alias || host) : host - series.push({ name: legend, value: d[type] || 0, host, isOther }) - iconRich[legend2LabelStyle(legend)] = { - height: LABEL_ICON_SIZE, - width: LABEL_ICON_SIZE, - fontSize: LABEL_ICON_SIZE, - backgroundColor: { image: d.iconUrl } - } + series.push({ name: legend, value: d[type] || 0, host, isOther, iconUrl }) + const richValue: PieLabelRichValueOption = { ...BASE_LABEL_RICH_VALUE } + iconUrl && (richValue.backgroundColor = { image: iconUrl }) + iconRich[legend2LabelStyle(legend)] = richValue }) options.series[0].data = series options.series[0].label.rich = { diff --git a/src/popup/components/footer/all-function.ts b/src/popup/components/footer/all-function.ts index 7907de11d..fae0a03fe 100644 --- a/src/popup/components/footer/all-function.ts +++ b/src/popup/components/footer/all-function.ts @@ -7,12 +7,11 @@ import { getAppPageUrl } from "@util/constant/url" import { t } from "@popup/locale" +import { createTab } from "@api/chrome/tab" function initAllFunction() { const allFunctionLink = document.getElementById('all-function-link') - allFunctionLink.onclick = async () => { - chrome.tabs.create({ url: getAppPageUrl(false, '/') }) - } + allFunctionLink.onclick = () => createTab(getAppPageUrl(false, '/')) allFunctionLink.innerText = t(msg => msg.base.allFunction) } diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 072fe9485..a2c58cbcc 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import type { FillFlagParam, TimerQueryParam } from "@service/timer-service" +import type { StatQueryParam } from "@service/stat-service" import initAllFunction from './all-function' import initUpgrade from './upgrade' @@ -13,22 +13,18 @@ import TotalInfoWrapper from "./total-info" import MergeHostWrapper from "./merge-host" import TimeSelectWrapper from "./select/time-select" import TypeSelectWrapper from "./select/type-select" -import timerService from "@service/timer-service" +import statService from "@service/stat-service" import { t } from "@popup/locale" -// Import from i18n import { locale } from "@i18n" import { getDayLenth, getMonthTime, getWeekDay, getWeekTime, MILL_PER_DAY } from "@util/time" import optionService from "@service/option-service" -import { IS_SAFARI } from "@util/constant/environment" -type FooterParam = TimerQueryParam & { +type FooterParam = StatQueryParam & { chartTitle: string } type QueryResultHandler = (result: PopupQueryResult) => void -const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: !IS_SAFARI, alias: true } - type DateRangeCalculator = (now: Date, weekStart: timer.option.WeekStartOption) => Date | [Date, Date] const dateRangeCalculators: { [duration in PopupDuration]: DateRangeCalculator } = { @@ -93,7 +89,7 @@ class FooterWrapper { const option = await optionService.getAllOption() as timer.option.PopupOption const itemCount = option.popupMax const queryParam = this.getQueryParam(option.weekStart) - const rows = await timerService.select(queryParam, FILL_FLAG_PARAM) + const rows = await statService.select(queryParam, true) const popupRows: PopupRow[] = [] const other: PopupRow = { host: t(msg => msg.chart.otherLabel, { count: 0 }), @@ -102,6 +98,7 @@ class FooterWrapper { time: 0, mergedHosts: [], isOther: true, + virtual: false } let otherCount = 0 for (let i = 0; i < rows.length; i++) { diff --git a/src/popup/components/footer/upgrade.ts b/src/popup/components/footer/upgrade.ts index e2a4d5605..a205af9f3 100644 --- a/src/popup/components/footer/upgrade.ts +++ b/src/popup/components/footer/upgrade.ts @@ -11,6 +11,7 @@ import { t } from "@popup/locale" import { UPDATE_PAGE } from "@util/constant/url" import { IS_FIREFOX } from "@util/constant/environment" import { IS_FROM_STORE } from "@util/constant/meta" +import { createTab } from "@api/chrome/tab" /** * Reset the position after upgrade showed @@ -57,7 +58,7 @@ function showUpgradeButton(latestVersion: string) { upgrade.classList.add("firefox-upgrade-no-underline") latestInfo.innerText = t(msg => msg.chart.updateVersionInfo4Firefox, { version: versionLabel }) } else { - upgradeLink.onclick = () => chrome.tabs.create({ url: UPDATE_PAGE }) + upgradeLink.onclick = () => createTab(UPDATE_PAGE) latestInfo.innerText = t(msg => msg.chart.updateVersionInfo, { version: versionLabel }) } } diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index 145d8af5b..70e7ad199 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { isIpAndPort } from "@util/pattern" +import { isIpAndPort, judgeVirtualFast } from "@util/pattern" import psl from "psl" /** @@ -65,19 +65,26 @@ export default class CustomizedHostMergeRuler implements timer.merge.Merger { * @returns merged host */ merge(origin: string): string { + let host = origin + if (judgeVirtualFast(origin)) { + host = origin.split('/')?.[0] + if (!host) { + return origin + } + } // First check the static rules - let merged = this.noRegMergeRules[origin] + let merged = this.noRegMergeRules[host] // Then check the regular rules let matchResult: undefined | RegRuleItem = undefined - merged === undefined && (matchResult = this.regulars.find(item => item.reg.test(origin))) + merged === undefined && (matchResult = this.regulars.find(item => item.reg.test(host))) matchResult && (merged = matchResult.result) if (merged === undefined) { // No rule matched - return isIpAndPort(origin) - ? origin - : psl.get(origin) || this.merge0(2, origin) + return isIpAndPort(host) + ? host + : psl.get(host) || this.merge0(2, host) } else { - return this.merge0(merged, origin) + return this.merge0(merged, host) } } diff --git a/src/service/components/immigration.ts b/src/service/components/immigration.ts index 6d5c2c092..5a9598b6a 100644 --- a/src/service/components/immigration.ts +++ b/src/service/components/immigration.ts @@ -8,13 +8,11 @@ import packageInfo from "@src/package" import BaseDatabase from "@db/common/base-database" import StoragePromise from "@db/common/storage-promise" -import IconUrlDatabase from "@db/icon-url-database" import LimitDatabase from "@db/limit-database" import MergeRuleDatabase from "@db/merge-rule-database" import PeriodDatabase from "@db/period-database" -import TimerDatabase from "@db/timer-database" +import StatDatabase from "@db/stat-database" import WhitelistDatabase from "@db/whitelist-database" -import HostAliasDatabase from "@db/host-alias-database" type MetaInfo = { version: string @@ -27,13 +25,11 @@ export type BackupData = { function initDatabase(storage: chrome.storage.StorageArea): BaseDatabase[] { const result: BaseDatabase[] = [ - new TimerDatabase(storage), - new IconUrlDatabase(storage), + new StatDatabase(storage), new PeriodDatabase(storage), new LimitDatabase(storage), new MergeRuleDatabase(storage), new WhitelistDatabase(storage), - new HostAliasDatabase(storage), ] return result diff --git a/src/service/components/virtual-site-holder.ts b/src/service/components/virtual-site-holder.ts new file mode 100644 index 000000000..72b0380dd --- /dev/null +++ b/src/service/components/virtual-site-holder.ts @@ -0,0 +1,63 @@ +import SiteDatabase from "@db/site-database" + +const siteDatabase = new SiteDatabase(chrome.storage.local) + +function compileAntPattern(antPattern: string): RegExp { + const segments = antPattern.split('/') + let patternStr = segments.map(seg => { + if (seg === "**") { + return ".*" + } else { + return seg.replace?.(/\*/g, "[^/]*").replace(/\./g, "\\.") + } + }).join("/") + // "google.com/**" => google\.com.* + if (patternStr.endsWith("/.*")) { + patternStr = patternStr.substring(0, patternStr.length - 3) + ".*" + } + + return new RegExp("^(.+://)?" + patternStr + "/?(\\?.*)?$") +} + +/** + * The singleton implementation of virtual sites holder + * + * @since 1.6.0 + */ +class VirtualSiteHolder { + hostSiteRegMap: Record = {} + + constructor() { + siteDatabase.select().then(sitesInfos => sitesInfos + .filter(s => s.virtual) + .forEach(site => this.updateRegularExp(site)) + ) + siteDatabase.addChangeListener((oldAndNew) => oldAndNew.forEach(([oldVal, newVal]) => { + if (!newVal) { + // deleted + delete this.hostSiteRegMap[oldVal.host] + } else { + this.updateRegularExp(newVal) + } + })) + } + + private updateRegularExp(siteInfo: timer.site.SiteInfo) { + const { host } = siteInfo + this.hostSiteRegMap[host] = compileAntPattern(host) + } + + /** + * Find the virtual sites which matches the target url + * + * @param url + * @returns virtul sites + */ + findMatched(url: string): string[] { + return Object.entries(this.hostSiteRegMap) + .filter(([_, reg]) => reg.test(url)) + .map(([k]) => k) + } +} + +export default new VirtualSiteHolder() \ No newline at end of file diff --git a/src/service/host-alias-service.ts b/src/service/host-alias-service.ts deleted file mode 100644 index 3e75a7128..000000000 --- a/src/service/host-alias-service.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import HostAliasDatabase, { HostAliasCondition } from "@db/host-alias-database" -import IconUrlDatabase from "@db/icon-url-database" -import { isRemainHost } from "@util/constant/remain-host" -import { slicePageResult } from "./components/page-info" - - -const storage = chrome.storage.local -const hostAliasDatabase = new HostAliasDatabase(storage) -const iconUrlDatabase = new IconUrlDatabase(storage) - -export type HostAliasQueryParam = HostAliasCondition - -class HostAliasService { - async selectByPage(param?: HostAliasQueryParam, page?: timer.common.PageQuery): Promise> { - const origin: timer.site.Alias[] = await hostAliasDatabase.select(param) - const result: timer.common.PageResult = slicePageResult(origin, page); - const list: timer.site.AliasIcon[] = result.list - await this.fillIconUrl(list) - return result - } - - async remove(host: timer.site.AliasKey): Promise { - await hostAliasDatabase.remove(host) - } - - async change(key: timer.site.AliasKey, name: string): Promise { - const toUpdate: timer.site.Alias = { ...key, name, source: 'USER' } - await hostAliasDatabase.update(toUpdate) - } - - exist(host: timer.site.AliasKey): Promise { - return hostAliasDatabase.exist(host) - } - - existBatch(hosts: timer.site.AliasKey[]): Promise { - return hostAliasDatabase.existBatch(hosts) - } - - /** - * @since 0.9.0 - */ - async get(host: timer.site.AliasKey): Promise { - const result = await hostAliasDatabase.get(host) - return result?.[0] - } - - private async fillIconUrl(items: timer.site.AliasIcon[]): Promise { - const need2Fill = items.filter(item => !item.merged && !isRemainHost(item.host)) - const hosts = need2Fill.map(o => o.host) - const iconUrlMap = await iconUrlDatabase.get(...hosts) - need2Fill.forEach(items => items.iconUrl = iconUrlMap[items.host]) - } -} - -export default new HostAliasService() diff --git a/src/service/limit-service.ts b/src/service/limit-service.ts index 8002a00b6..ae40032fb 100644 --- a/src/service/limit-service.ts +++ b/src/service/limit-service.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { listTabs, sendMsg2Tab } from "@api/chrome/tab" import { DATE_FORMAT } from "@db/common/constant" import LimitDatabase from "@db/limit-database" import TimeLimitItem from "@entity/time-limit-item" @@ -43,16 +44,12 @@ async function select(cond?: QueryParam): Promise { */ async function handleLimitChanged() { const allItems: TimeLimitItem[] = await select({ filterDisabled: false, url: undefined }) - chrome.tabs.query({}, tabs => tabs.forEach(tab => { + const tabs = await listTabs() + tabs.forEach(tab => { const limitedItems = allItems.filter(item => item.matches(tab.url) && item.enabled && item.hasLimited()) - chrome.tabs.sendMessage, timer.mq.Response>(tab.id, { - code: 'limitChanged', - data: limitedItems - }, _result => { - const error = chrome.runtime.lastError - error && console.log(error.message) - }) - })) + sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) + .catch(err => console.log(err.message)) + }) } async function updateEnabled(item: timer.limit.Item): Promise { @@ -137,4 +134,4 @@ class LimitService { } } -export default new LimitService() \ No newline at end of file +export default new LimitService() diff --git a/src/service/meta-service.ts b/src/service/meta-service.ts index aeb6593a4..3eb26331d 100644 --- a/src/service/meta-service.ts +++ b/src/service/meta-service.ts @@ -11,12 +11,12 @@ const storage = chrome.storage.local const db: MetaDatabase = new MetaDatabase(storage) async function getInstallTime() { - const meta: timer.meta.ExtensionMeta = await db.getMeta() + const meta: timer.ExtensionMeta = await db.getMeta() return meta && meta.installTime ? new Date(meta.installTime) : undefined } async function updateInstallTime(installTime: Date) { - const meta: timer.meta.ExtensionMeta = await db.getMeta() + const meta: timer.ExtensionMeta = await db.getMeta() if (meta?.installTime) { // Must not rewrite return @@ -44,7 +44,7 @@ function increasePopup(): void { } async function getCid(): Promise { - const meta: timer.meta.ExtensionMeta = await db.getMeta() + const meta: timer.ExtensionMeta = await db.getMeta() return meta?.cid } diff --git a/src/service/site-service.ts b/src/service/site-service.ts new file mode 100644 index 000000000..aad393fb8 --- /dev/null +++ b/src/service/site-service.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import SiteDatabase, { SiteCondition } from "@db/site-database" +import { slicePageResult } from "./components/page-info" + +const storage = chrome.storage.local +const siteDatabase = new SiteDatabase(storage) + +export type SiteQueryParam = SiteCondition + +async function removeAlias(this: SiteService, key: timer.site.SiteKey) { + const exist = await siteDatabase.get(key) + if (!exist) return + delete exist.alias + delete exist.source + await siteDatabase.save(exist) +} + +async function saveAlias(this: SiteService, key: timer.site.SiteKey, alias: string, source: timer.site.AliasSource) { + const exist = await siteDatabase.get(key) + let toUpdate: timer.site.SiteInfo + if (exist) { + // Can't overwrite existed by user + const canSave = source === 'USER' || exist.source !== 'USER' + if (!canSave) return + toUpdate = exist + toUpdate.alias = alias + toUpdate.source = source + } else { + toUpdate = { ...key, alias, source } + } + await siteDatabase.save(toUpdate) +} + +async function saveIconUrl(this: SiteService, key: timer.site.SiteKey, iconUrl: string) { + const exist = await siteDatabase.get(key) + let toUpdate: timer.site.SiteInfo + if (exist) { + toUpdate = { ...exist } + toUpdate.iconUrl = iconUrl + } else { + toUpdate = { ...key, iconUrl } + } + await siteDatabase.save(toUpdate) +} + +class SiteService { + async add(siteInfo: timer.site.SiteInfo): Promise { + if (await siteDatabase.exist(siteInfo)) { + return + } + await siteDatabase.save(siteInfo) + } + + async selectByPage(param?: SiteQueryParam, page?: timer.common.PageQuery): Promise> { + const origin: timer.site.SiteInfo[] = await siteDatabase.select(param) + const result: timer.common.PageResult = slicePageResult(origin, page); + return result + } + + async batchSelect(keys: timer.site.SiteKey[]): Promise { + return siteDatabase.getBatch(keys) + } + + async remove(host: timer.site.SiteKey): Promise { + await siteDatabase.remove(host) + } + + saveAlias = saveAlias + + removeAlias = removeAlias + + saveIconUrl = saveIconUrl + + exist(host: timer.site.SiteKey): Promise { + return siteDatabase.exist(host) + } + + existBatch(hosts: timer.site.SiteKey[]): Promise { + return siteDatabase.existBatch(hosts) + } + + /** + * @since 0.9.0 + */ + async get(host: timer.site.SiteKey): Promise { + return await siteDatabase.get(host) + } +} + +export default new SiteService() diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts new file mode 100644 index 000000000..1c16dcb56 --- /dev/null +++ b/src/service/stat-service/index.ts @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import StatDatabase, { StatCondition } from "@db/stat-database" +import { log } from "../../common/logger" +import CustomizedHostMergeRuler from "../components/host-merge-ruler" +import MergeRuleDatabase from "@db/merge-rule-database" +import { slicePageResult } from "../components/page-info" +import whitelistHolder from '../components/whitelist-holder' +import { resultOf } from "@util/stat" +import SiteDatabase from "@db/site-database" +import { mergeDate, mergeHost } from "./merge" +import virtualSiteHolder from "@service/components/virtual-site-holder" +import { judgeVirtualFast } from "@util/pattern" +import { canReadRemote, processRemote } from "./remote" + +const storage = chrome.storage.local + +const statDatabase = new StatDatabase(storage) +const mergeRuleDatabase = new MergeRuleDatabase(storage) +const siteDatabase = new SiteDatabase(storage) + +export type SortDirect = 'ASC' | 'DESC' + +export type StatQueryParam = StatCondition & { + /** + * Inclusive remote data + * + * If true the date range MUST NOT be unlimited + * + * @since 1.2.0 + */ + inclusiveRemote?: boolean + /** + * Group by the root host + */ + mergeHost?: boolean + /** + * Merge items of the same host from different days + */ + mergeDate?: boolean + /** + * The name of sorted column + */ + sort?: keyof timer.stat.Row + /** + * 1 asc, -1 desc + */ + sortOrder?: SortDirect +} + +export type HostSet = { + origin: Set + merged: Set + virtual: Set +} + +function calcFocusInfo(timeInfo: TimeInfo): number { + return Object.values(timeInfo).reduce((a, b) => a + b, 0) +} + +function calcVirtualFocusInfo(data: { [host: string]: TimeInfo }): Record { + const container: Record = {} + Object.values(data).forEach(timeInfo => Object.entries(timeInfo).forEach(([url, focusTime]) => { + const virtualHosts = virtualSiteHolder.findMatched(url) + virtualHosts.forEach(virtualHost => (container[virtualHost] = (container[virtualHost] || 0) + focusTime)) + })) + const result: Record = {} + Object.entries(container).forEach(([host, focusTime]) => result[host] = resultOf(focusTime, 0)) + return result +} + +/** + * Service of timer + * @since 0.0.5 + */ +class StatService { + + async addFocusTime(data: { [host: string]: TimeInfo }): Promise { + // 1. normal sites + const normalFocusInfo: Record = {} + Object.entries(data) + .filter(([host]) => whitelistHolder.notContains(host)) + .forEach(([host, timeInfo]) => normalFocusInfo[host] = resultOf(calcFocusInfo(timeInfo), 0)) + // 2. virtual sites + const virtualFocusInfo: Record = calcVirtualFocusInfo(data) + return statDatabase.accumulateBatch({ ...normalFocusInfo, ...virtualFocusInfo }, new Date()) + } + + async addOneTime(host: string) { + statDatabase.accumulate(host, new Date(), resultOf(0, 1)) + } + + /** + * Query hosts + * + * @param fuzzyQuery the part of host + * @since 0.0.8 + */ + async listHosts(fuzzyQuery: string): Promise { + const rows = await statDatabase.select() + const allHosts: Set = new Set() + rows.map(row => row.host).forEach(host => allHosts.add(host)) + // Generate ruler + const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() + const mergeRuler = new CustomizedHostMergeRuler(mergeRuleItems) + + const origin: Set = new Set() + const merged: Set = new Set() + + const allHostArr = Array.from(allHosts) + + allHostArr.forEach(host => { + if (judgeVirtualFast(host)) { + return + } + host.includes(fuzzyQuery) && origin.add(host) + const mergedHost = mergeRuler.merge(host) + mergedHost?.includes(fuzzyQuery) && merged.add(mergedHost) + }) + + const virtualSites = await siteDatabase.select({ virtual: true }) + const virtual: Set = new Set(virtualSites.map(site => site.host)) + return { origin, merged, virtual } + } + + /** + * Count the items + * + * @param condition condition to count + * @since 1.0.2 + */ + async count(condition: StatCondition): Promise { + log("service: count: {condition}", condition) + const count = await statDatabase.count(condition) + log("service: count: {result}", count) + return count + } + + private processSort(origin: timer.stat.Row[], param: StatQueryParam) { + const { sort, sortOrder } = param + if (!sort) return + + const order = sortOrder || 'ASC' + origin.sort((a, b) => { + const aa = a[sort] + const bb = b[sort] + if (aa === bb) return 0 + return (order === 'ASC' ? 1 : -1) * (aa > bb ? 1 : -1) + }) + } + + private async fillSiteInfo(items: timer.stat.Row[], mergeHost: boolean) { + const keys: timer.site.SiteKey[] = items.map(({ host }) => ({ host, merged: mergeHost, virtual: judgeVirtualFast(host) })) + const siteInfos = await siteDatabase.getBatch(keys) + const siteInfoMap: Record = {} + siteInfos.forEach(siteInfo => { + const { host, merged, virtual } = siteInfo + const key = `${merged ? 1 : 0}${virtual ? 1 : 0}${host}` + siteInfoMap[key] = siteInfo + }) + items.forEach(item => { + const { host } = item + const key = `${mergeHost ? 1 : 0}${judgeVirtualFast(host) ? 1 : 0}${host}` + const siteInfo = siteInfoMap[key] + if (siteInfo) { + item.iconUrl = siteInfo.iconUrl + item.alias = siteInfo.alias + } + }) + } + + async select(param?: StatQueryParam, fillSiteInfo?: boolean): Promise { + log("service: select:{param}", param) + + // Need match full host after merged + let fullHost = undefined + // If merged and full host + // Then set the host blank + // And filter them after merge + param?.mergeHost && param?.fullHost && !(param.fullHost = false) && (fullHost = param?.host) && (param.host = undefined) + + param = param || {} + let origin = await statDatabase.select(param as StatCondition) + if (param.inclusiveRemote) { + origin = await processRemote(param, origin) + } + // Process after select + // 1st merge + if (param.mergeHost) { + // Merge with rules + origin = await mergeHost(origin) + // filter again, cause of the exchange of the host, if the param.mergeHost is true + origin = this.filter(origin, param) + } + param.mergeDate && (origin = mergeDate(origin)) + // 2nd sort + this.processSort(origin, param) + // 3rd get icon url and alias if need + fillSiteInfo && await this.fillSiteInfo(origin, param.mergeHost) + // Filter merged host if full host + fullHost && (origin = origin.filter(dataItem => dataItem.host === fullHost)) + return origin + } + + getResult(host: string, date: Date): Promise { + return statDatabase.get(host, date) + } + + async selectByPage( + param?: StatQueryParam, + page?: timer.common.PageQuery, + fillSiteInfo?: boolean + ): Promise> { + log("selectByPage:{param},{page}", param, page) + // Not fill at first + const origin: timer.stat.Row[] = await this.select(param, fillSiteInfo) + const result: timer.common.PageResult = slicePageResult(origin, page) + const list = result.list + // Filter after page sliced + if (fillSiteInfo && param?.mergeHost) { + for (const beforeMerge of list) await this.fillSiteInfo(beforeMerge.mergedHosts, true) + } + log("result of selectByPage:{param}, {page}, {result}", param, page, result) + return result + } + + private filter(origin: timer.stat.Row[], param: StatCondition) { + const paramHost = (param.host || '').trim() + return paramHost ? origin.filter(o => o.host.includes(paramHost)) : origin + } + + /** + * Aable to read remote backup data + * + * @since 1.2.0 + * @returns T/F + */ + canReadRemote = canReadRemote +} + +export default new StatService() \ No newline at end of file diff --git a/src/service/timer-service/merge.ts b/src/service/stat-service/merge.ts similarity index 99% rename from src/service/timer-service/merge.ts rename to src/service/stat-service/merge.ts index 3d4cfb392..8594a2b40 100644 --- a/src/service/timer-service/merge.ts +++ b/src/service/stat-service/merge.ts @@ -23,6 +23,7 @@ function merge(map: Record, origin: timer.stat.Row, key: composition: { focus: [], time: [] }, cid: origin.cid, cname: origin.cname, + virtual: false }) exist.time += origin.time diff --git a/src/service/stat-service/remote.ts b/src/service/stat-service/remote.ts new file mode 100644 index 000000000..5559263f8 --- /dev/null +++ b/src/service/stat-service/remote.ts @@ -0,0 +1,91 @@ +import OptionDatabase from "@db/option-database" +import { StatCondition } from "@db/stat-database" +import processor from "@src/common/backup/processor" +import { judgeVirtualFast } from "@util/pattern" +import { getBirthday } from "@util/time" + +const optionDatabase = new OptionDatabase(chrome.storage.local) + +const keyOf = (row: timer.stat.RowKey) => `${row.date}${row.host}` + +export async function processRemote(param: StatCondition, origin: timer.stat.Row[]): Promise { + const { backupType, backupAuths } = await optionDatabase.getOption() + const auth = backupAuths?.[backupType] + const canReadRemote = await canReadRemote0(backupType, auth) + if (!canReadRemote) { + return origin + } + // Map to merge + const originMap: Record = {} + origin.forEach(row => originMap[keyOf(row)] = { + ...row, + composition: { + focus: [row.focus], + time: [row.time], + } + }) + // Predicate with host + const { host, fullHost } = param + const predicate: (row: timer.stat.RowBase) => boolean = host + // With host condition + ? fullHost + // Full match + ? r => r.host === host + // Fuzzy match + : r => r.host && r.host.includes(host) + // Without host condition + : _r => true + // 1. query remote + let start: Date = undefined, end: Date = undefined + if (param.date instanceof Array) { + start = param.date?.[0] + end = param.date?.[1] + } else { + start = param.date + } + start = start || getBirthday() + end = end || new Date() + const remote = await processor.query(backupType, auth, start, end) + remote.filter(predicate).forEach(row => processRemoteRow(originMap, row)) + return Object.values(originMap) +} + +/** + * Aable to read remote backup data + * + * @since 1.2.0 + * @returns T/F + */ +export async function canReadRemote(): Promise { + const { backupType, backupAuths } = await optionDatabase.getOption() + return await canReadRemote0(backupType, backupAuths?.[backupType]) +} + +async function canReadRemote0(backupType: timer.backup.Type, auth: string): Promise { + return backupType && backupType !== 'none' && !await processor.test(backupType, auth) +} + +function processRemoteRow(rowMap: Record, row: timer.stat.Row) { + const key = keyOf(row) + let exist = rowMap[key] + !exist && (exist = rowMap[key] = { + date: row.date, + host: row.host, + time: 0, + focus: 0, + composition: { + focus: [], + time: [], + }, + mergedHosts: [], + virtual: judgeVirtualFast(row.host) + }) + + const focus = row.focus || 0 + const time = row.time || 0 + + exist.focus += focus + exist.time += time + focus && exist.composition.focus.push({ cid: row.cid, cname: row.cname, value: focus }) + time && exist.composition.time.push({ cid: row.cid, cname: row.cname, value: time }) +} \ No newline at end of file diff --git a/src/service/timer-service/index.ts b/src/service/timer-service/index.ts deleted file mode 100644 index abc010168..000000000 --- a/src/service/timer-service/index.ts +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import TimerDatabase, { TimerCondition } from "@db/timer-database" -import { log } from "../../common/logger" -import CustomizedHostMergeRuler from "../components/host-merge-ruler" -import MergeRuleDatabase from "@db/merge-rule-database" -import IconUrlDatabase from "@db/icon-url-database" -import HostAliasDatabase from "@db/host-alias-database" -import { slicePageResult } from "../components/page-info" -import whitelistHolder from '../components/whitelist-holder' -import { resultOf } from "@util/stat" -import OptionDatabase from "@db/option-database" -import processor from "@src/common/backup/processor" -import { getBirthday } from "@util/time" -import { mergeDate, mergeHost } from "./merge" - -const storage = chrome.storage.local - -const timerDatabase = new TimerDatabase(storage) -const iconUrlDatabase = new IconUrlDatabase(storage) -const hostAliasDatabase = new HostAliasDatabase(storage) -const mergeRuleDatabase = new MergeRuleDatabase(storage) -const optionDatabase = new OptionDatabase(storage) - -export type SortDirect = 'ASC' | 'DESC' - -export type TimerQueryParam = TimerCondition & { - /** - * Inclusive remote data - * - * If true the date range MUST NOT be unlimited - * - * @since 1.2.0 - */ - inclusiveRemote?: boolean - /** - * Group by the root host - */ - mergeHost?: boolean - /** - * Merge items of the same host from different days - */ - mergeDate?: boolean - /** - * The name of sorted column - */ - sort?: keyof timer.stat.Row - /** - * 1 asc, -1 desc - */ - sortOrder?: SortDirect -} - -/** - * @since 0.5.0 - */ -export type FillFlagParam = { - /** - * Whether to fill the icon url - */ - iconUrl?: boolean - /** - * Whether to fill the alias - */ - alias?: boolean -} - -export type HostSet = { - origin: Set - merged: Set -} - -function calcFocusInfo(timeInfo: TimeInfo): number { - return Object.values(timeInfo).reduce((a, b) => a + b, 0) -} - -const keyOf = (row: timer.stat.RowKey) => `${row.date}${row.host}` - -async function processRemote(param: TimerCondition, origin: timer.stat.Row[]): Promise { - const { backupType, backupAuths } = await optionDatabase.getOption() - const auth = backupAuths?.[backupType] - const canReadRemote = await canReadRemote0(backupType, auth) - if (!canReadRemote) { - return origin - } - // Map to merge - const originMap: Record = {} - origin.forEach(row => originMap[keyOf(row)] = { - ...row, - composition: { - focus: [row.focus], - time: [row.time], - } - }) - // Predicate with host - const { host, fullHost } = param - const predicate: (row: timer.stat.RowBase) => boolean = host - // With host condition - ? fullHost - // Full match - ? r => r.host === host - // Fuzzy match - : r => r.host && r.host.includes(host) - // Without host condition - : _r => true - // 1. query remote - let start: Date = undefined, end: Date = undefined - if (param.date instanceof Array) { - start = param.date?.[0] - end = param.date?.[1] - } else { - start = param.date - } - start = start || getBirthday() - end = end || new Date() - const remote = await processor.query(backupType, auth, start, end) - remote.filter(predicate).forEach(row => processRemoteRow(originMap, row)) - return Object.values(originMap) -} - -function processRemoteRow(rowMap: Record, row: timer.stat.Row) { - const key = keyOf(row) - let exist = rowMap[key] - !exist && (exist = rowMap[key] = { - date: row.date, - host: row.host, - time: 0, - focus: 0, - composition: { - focus: [], - time: [], - }, - mergedHosts: [], - }) - - const focus = row.focus || 0 - const time = row.time || 0 - - exist.focus += focus - exist.time += time - focus && exist.composition.focus.push({ cid: row.cid, cname: row.cname, value: focus }) - time && exist.composition.time.push({ cid: row.cid, cname: row.cname, value: time }) -} - - -async function canReadRemote0(backupType: timer.backup.Type, auth: string): Promise { - return backupType && backupType !== 'none' && !await processor.test(backupType, auth) -} - -/** - * Service of timer - * @since 0.0.5 - */ -class TimerService { - - async addFocusAndTotal(data: { [host: string]: TimeInfo }): Promise { - const toUpdate = {} - Object.entries(data) - .filter(([host]) => whitelistHolder.notContains(host)) - .forEach(([host, timeInfo]) => toUpdate[host] = resultOf(calcFocusInfo(timeInfo), 0)) - return timerDatabase.accumulateBatch(toUpdate, new Date()) - } - - async addOneTime(host: string) { - timerDatabase.accumulate(host, new Date(), resultOf(0, 1)) - } - - /** - * Query hosts - * - * @param fuzzyQuery the part of host - * @since 0.0.8 - */ - async listHosts(fuzzyQuery: string): Promise { - const rows = await timerDatabase.select() - const allHosts: Set = new Set() - rows.map(row => row.host).forEach(host => allHosts.add(host)) - // Generate ruler - const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() - const mergeRuler = new CustomizedHostMergeRuler(mergeRuleItems) - - const origin: Set = new Set() - const merged: Set = new Set() - - const allHostArr = Array.from(allHosts) - allHostArr - .filter(host => host.includes(fuzzyQuery)) - .forEach(host => origin.add(host)) - allHostArr - .map(host => mergeRuler.merge(host)) - .filter(host => host.includes(fuzzyQuery)) - .forEach(host => merged.add(host)) - - return { origin, merged } - } - - /** - * Count the items - * - * @param condition condition to count - * @since 1.0.2 - */ - async count(condition: TimerCondition): Promise { - log("service: count: {condition}", condition) - const count = await timerDatabase.count(condition) - log("service: count: {result}", count) - return count - } - - private processSort(origin: timer.stat.Row[], param: TimerQueryParam) { - const { sort, sortOrder } = param - if (!sort) return - - const order = sortOrder || 'ASC' - origin.sort((a, b) => { - const aa = a[sort] - const bb = b[sort] - if (aa === bb) return 0 - return (order === 'ASC' ? 1 : -1) * (aa > bb ? 1 : -1) - }) - } - - private async fillIconUrl(items: timer.stat.Row[]): Promise { - const hosts = items.map(o => o.host) - const iconUrlMap = await iconUrlDatabase.get(...hosts) - items.forEach(dataItem => dataItem.iconUrl = iconUrlMap[dataItem.host]) - } - - private async fillAlias(items: timer.stat.Row[], mergeHost: boolean): Promise { - const keys = items.map(({ host }) => ({ host, merged: mergeHost })) - const allAlias = await hostAliasDatabase.get(...keys) - const aliasMap = {} - allAlias.forEach(({ host, name }) => aliasMap[host] = name) - items.forEach(dataItem => dataItem.alias = aliasMap[dataItem.host]) - } - - async select(param?: TimerQueryParam, flagParam?: FillFlagParam): Promise { - log("service: select:{param}", param) - - // Need match full host after merged - let fullHost = undefined - // If merged and full host - // Then set the host blank - // And filter them after merge - param?.mergeHost && param?.fullHost && !(param.fullHost = false) && (fullHost = param?.host) && (param.host = undefined) - - param = param || {} - let origin = await timerDatabase.select(param as TimerCondition) - if (param.inclusiveRemote) { - origin = await processRemote(param, origin) - } - // Process after select - // 1st merge - if (param.mergeHost) { - // Merge with rules - origin = await mergeHost(origin) - // filter again, cause of the exchange of the host, if the param.mergeHost is true - origin = this.filter(origin, param) - } - param.mergeDate && (origin = mergeDate(origin)) - // 2nd sort - this.processSort(origin, param) - // 3rd get icon url and alias if need - flagParam?.alias && await this.fillAlias(origin, param.mergeHost) - if (!param.mergeHost) { - flagParam?.iconUrl && await this.fillIconUrl(origin) - } - // Filter merged host if full host - fullHost && (origin = origin.filter(dataItem => dataItem.host === fullHost)) - return origin - } - - getResult(host: string, date: Date): Promise { - return timerDatabase.get(host, date) - } - - async selectByPage( - param?: TimerQueryParam, - page?: timer.common.PageQuery, - fillFlag?: FillFlagParam - ): Promise> { - log("selectByPage:{param},{page}", param, page) - // Not fill at first - const origin: timer.stat.Row[] = await this.select(param) - const result: timer.common.PageResult = slicePageResult(origin, page) - const list = result.list - // Filter after page sliced - if (fillFlag?.iconUrl) { - if (param?.mergeHost) { - for (const beforeMerge of list) await this.fillIconUrl(beforeMerge.mergedHosts) - } else { - await this.fillIconUrl(list) - } - } - if (fillFlag?.alias) { - await this.fillAlias(list, param.mergeHost) - } - return result - } - - private filter(origin: timer.stat.Row[], param: TimerCondition) { - const paramHost = (param.host || '').trim() - return paramHost ? origin.filter(o => o.host.includes(paramHost)) : origin - } - - /** - * Aable to read remote backup data - * - * @since 1.2.0 - * @returns T/F - */ - async canReadRemote(): Promise { - const { backupType, backupAuths } = await optionDatabase.getOption() - return await canReadRemote0(backupType, backupAuths?.[backupType]) - } -} - -export default new TimerService() \ No newline at end of file diff --git a/src/util/constant/environment.ts b/src/util/constant/environment.ts index 83c108d06..6d82b562a 100644 --- a/src/util/constant/environment.ts +++ b/src/util/constant/environment.ts @@ -57,3 +57,5 @@ export const IS_SAFARI: boolean = isSafari * @since 1.3.2 */ export const BROWSER_MAJOR_VERSION = browserMajorVersion + +export const IS_MV3 = chrome.runtime.getManifest().manifest_version === 3 \ No newline at end of file diff --git a/src/util/constant/meta.ts b/src/util/constant/meta.ts index 4ca80d93b..35430ba9e 100644 --- a/src/util/constant/meta.ts +++ b/src/util/constant/meta.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { getRuntimeId } from "@api/chrome/runtime" import { IS_CHROME, IS_EDGE, IS_FIREFOX } from "./environment" /** @@ -22,7 +23,7 @@ export const FIREFOX_ID = "{a8cf72f7-09b7-4cd4-9aaa-7a023bf09916}" */ export const EDGE_ID = "fepjgblalcnepokjblgbgmapmlkgfahc" -const id = chrome.runtime.id +const id = getRuntimeId() /** * @since 0.9.6 diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 966db8498..d4e5aaaac 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { getRuntimeId } from "@api/chrome/runtime" import { IS_FIREFOX, IS_CHROME, IS_EDGE } from "./environment" export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/zh-CN/firefox/addon/web%E6%99%82%E9%96%93%E7%B5%B1%E8%A8%88/' @@ -33,7 +34,7 @@ export const SOURCE_CODE_PAGE = 'https://github.com/sheepzh/timer' /** * @since 0.0.6 */ -export const GITHUB_ISSUE_ADD = 'https://github.com/sheepzh/timer/issues/new' +export const GITHUB_ISSUE_ADD = 'https://github.com/sheepzh/timer/issues/new/choose' /** * Feedback powered by www.wjx.cn @@ -75,7 +76,7 @@ export const UNINSTALL_QUESTIONNAIRE: { [locale in timer.Locale]: string } = { let updatePage = SOURCE_CODE_PAGE if (IS_CHROME) { - updatePage = `chrome://extensions/?id=${chrome.runtime.id}` + updatePage = `chrome://extensions/?id=${getRuntimeId()}` } else if (IS_EDGE) { // In the management page with developing-mode open updatePage = 'edge://extensions' @@ -84,8 +85,6 @@ if (IS_CHROME) { export const UPDATE_PAGE = updatePage /** - * chrome.tabs.create({ url: getAppPageUrl() }) - * * @param isInBackground invoke in background environment * @since 0.2.2 */ @@ -113,7 +112,8 @@ export function getGuidePageUrl(isInBackground: boolean): string { export function iconUrlOfBrowser(protocol: string, host: string): string { if (IS_CHROME || IS_EDGE) { return `${IS_CHROME ? 'chrome' : 'edge'}://favicon/${protocol ? protocol + '://' : ''}${host}` - } else return '' + } + return undefined } /** diff --git a/src/util/pattern.ts b/src/util/pattern.ts index ea3910ade..77e7d4a33 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -53,6 +53,9 @@ export function isIpAndPort(host: string) { * @param host */ export function isValidHost(host: string) { + if (!host) return false + if (host.includes('/') || host.includes('?')) return false + const indexOfColon = host.indexOf(':') if (indexOfColon > -1) { const portStr = host.substring(indexOfColon + 1) @@ -65,6 +68,43 @@ export function isValidHost(host: string) { return reg.test(host) } +/** + * Test whether the host is a valid virtual host + * + * github.com/ = false + * github.com = false + * github.com/sheepzh = true + * github.com/* = true + * github.com/** = true + * github.com/sheepzh/ = false + * github.com/sheepzh? = false + * github.com/sheepzh?a=1 = false + * http://github.com/123 = false + * + * @since 1.6.0 + */ +export function isValidVirtualHost(host: string) { + if (!host) return false + if (host.includes('?') || host.includes('=') || host.includes(":")) return false + // Can't ends with / + if (host.endsWith('/')) return false + const segs = host.split('/') + // Can't be normal host + if (segs.length === 1) return false + if (!isValidHost(segs[0])) return false + return true +} + +/** + * Judge virtual host fastly + * + * @param host + * @returns T/F + */ +export function judgeVirtualFast(host: string): boolean { + return host?.includes('/') +} + export type HostInfo = { /** * Including port diff --git a/src/util/site.ts b/src/util/site.ts index eb623cdd2..dee2d11f9 100644 --- a/src/util/site.ts +++ b/src/util/site.ts @@ -33,7 +33,7 @@ export function extractSiteName(title: string, host?: string) { .split(SEPARATORS) .filter(s => !INVALID_SITE_NAME.test(s)) .sort((a, b) => a.length - b.length)[0] - .trim() + ?.trim?.() } /** @@ -47,4 +47,4 @@ export function generateSiteLabel(host: string, name?: string): string { } else { return host } -} \ No newline at end of file +} diff --git a/src/util/stat.ts b/src/util/stat.ts index 2bf0357eb..c64092bd9 100644 --- a/src/util/stat.ts +++ b/src/util/stat.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import { judgeVirtualFast } from "./pattern" + export function isNotZeroResult(target: timer.stat.Result): boolean { return !!target.focus || !!target.time } @@ -23,11 +25,11 @@ export function resultOf(focus: number, time: number): timer.stat.Result { export function rowOf(key: timer.stat.RowKey, item?: timer.stat.Result): timer.stat.Row { return { - host: key.host, - date: key.date, + ...key, focus: item && item.focus || 0, time: item && item.time || 0, - mergedHosts: [] + mergedHosts: [], + virtual: judgeVirtualFast(key.host), } } diff --git a/test/background/backup/gist/compressor.test.ts b/test/background/backup/gist/compressor.test.ts index 642fc5aff..a08373647 100644 --- a/test/background/backup/gist/compressor.test.ts +++ b/test/background/backup/gist/compressor.test.ts @@ -6,14 +6,16 @@ test('devide 1', () => { date: '20220801', focus: 0, time: 10, - mergedHosts: [] + mergedHosts: [], + virtual: false, }, { host: 'www.baidu.com', // Invalid date, count be compress date: '', focus: 0, time: 10, - mergedHosts: [] + mergedHosts: [], + virtual: false, }] const devided = devide2Buckets(rows) expect(devided.length).toEqual(1) diff --git a/test/database/icon-url-database.test.ts b/test/database/icon-url-database.test.ts deleted file mode 100644 index 21ffa17c8..000000000 --- a/test/database/icon-url-database.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import IconUrlDatabase from "@db/icon-url-database" -import storage from "../__mock__/storage" - -const db = new IconUrlDatabase(storage.local) - -const baidu = 'baidu.com' - -describe('icon-url-database', () => { - beforeEach(async () => { - await storage.local.clear() - // Mock Chrome - const mockUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36" - Object.defineProperty(global.navigator, 'userAgent', { value: mockUserAgent, configurable: true }) - }) - - test('1', async () => { - await db.put(baidu, 'test1') - expect((await db.get(baidu))[baidu]).toEqual('test1') - await db.put(baidu, 'test2') - expect((await db.get(baidu))[baidu]).toEqual('test2') - let foo = 'baidu123213131' - expect((await db.get(foo))[foo]).toBeUndefined() - }) - - test("import data", async () => { - await db.put(baidu, "test1") - const data2Import = { - "__timer__ICON_URLbaidu.com": "test0", - "__timer__ICON_URLwww.qq.com": "test2", - // Invalid icon url - "_timer__ICON_URLwww.qq.com": "1111", - // Not import - "__timer__ICON_URLgoogle.com": "chrome://favicon/google.com" - } - await db.importData(data2Import) - const items = await db.storage.get() - expect(Object.values(items).length).toEqual(2) - // Not overwrite - const baiduIconUrl = (await db.get(baidu))[baidu] - expect(baiduIconUrl).toEqual('test1') - const qqIconUrl = (await db.get("www.qq.com"))["www.qq.com"] - expect(qqIconUrl).toEqual('test2') - // Not import - const googleIconUrl = (await db.get("google.com"))["google.com"] - expect(googleIconUrl).toBeUndefined() - }) -}) \ No newline at end of file diff --git a/test/database/timer-database.test.ts b/test/database/timer-database.test.ts index 9a5c3923f..53a6cbf97 100644 --- a/test/database/timer-database.test.ts +++ b/test/database/timer-database.test.ts @@ -1,10 +1,10 @@ import { DATE_FORMAT } from "@db/common/constant" -import TimerDatabase, { TimerCondition } from "@db/timer-database" +import StatDatabase, { StatCondition } from "@db/stat-database" import { formatTime, MILL_PER_DAY } from "@util/time" import { resultOf } from "@util/stat" import storage from "../__mock__/storage" -const db = new TimerDatabase(storage.local) +const db = new StatDatabase(storage.local) const now = new Date() const nowStr = formatTime(now, DATE_FORMAT) const yesterday = new Date(now.getTime() - MILL_PER_DAY) @@ -56,7 +56,7 @@ describe('timer-database', () => { ) expect((await db.select()).length).toEqual(6) - let cond: TimerCondition = {} + let cond: StatCondition = {} cond.host = 'google' let list = await db.select(cond) @@ -73,8 +73,8 @@ describe('timer-database', () => { cond = {} cond.date = [now, now] const expectedResult: timer.stat.Row[] = [ - { date: nowStr, focus: 11, host: google, mergedHosts: [], time: 0 }, - { date: nowStr, focus: 1, host: baidu, mergedHosts: [], time: 0 } + { date: nowStr, focus: 11, host: google, mergedHosts: [], time: 0, virtual: false }, + { date: nowStr, focus: 1, host: baidu, mergedHosts: [], time: 0, virtual: false } ] expect(await db.select(cond)).toEqual(expectedResult) // Only use start @@ -117,7 +117,7 @@ describe('timer-database', () => { expect((await db.select()).length).toEqual(3) // Delete all the baidu await db.deleteByUrl(baidu) - const cond: TimerCondition = { host: baidu, fullHost: true } + const cond: StatCondition = { host: baidu, fullHost: true } // Nothing of baidu remained expect((await db.select(cond)).length).toEqual(0) // But google remained diff --git a/test/util/pattern.test.ts b/test/util/pattern.test.ts index 64bc57df8..ad9320324 100644 --- a/test/util/pattern.test.ts +++ b/test/util/pattern.test.ts @@ -1,5 +1,5 @@ import { JSON_HOST, PDF_HOST, PIC_HOST, TXT_HOST } from "@util/constant/remain-host" -import { extractFileHost, extractHostname, isBrowserUrl, isHomepage, isIpAndPort, isValidHost } from "@util/pattern" +import { extractFileHost, extractHostname, isBrowserUrl, isHomepage, isIpAndPort, isValidHost, isValidVirtualHost } from "@util/pattern" test('browser url', () => { // chrome @@ -28,6 +28,9 @@ test('ip and port', () => { }) test('merge host origin', () => { + expect(isValidHost('')).toBeFalsy() + expect(isValidHost(undefined)).toBeFalsy() + expect(isValidHost('wwdad.basd.com.111:12345')).toBeTruthy() expect(isValidHost('wwdad.basd.com.a111a:12345')).toBeTruthy() expect(isValidHost('wwdad.basd.com.a111a:*')).toBeTruthy() @@ -37,6 +40,8 @@ test('merge host origin', () => { expect(isValidHost('wwdad.basd..*')).toBeFalsy() expect(isValidHost('wwdad*.*')).toBeFalsy() expect(isValidHost('wwdad.*.*')).toBeTruthy() + + expect(isValidHost('https://ww.baidcom')).toBeFalsy() }) test("url", () => { @@ -79,4 +84,18 @@ test("extractFileHost", () => { expect(extractFileHost("file://123json")).toEqual(undefined) expect(extractFileHost("file://123.html")).toEqual(undefined) expect(extractFileHost("file://123.")).toEqual(undefined) +}) + +test("valid virtual host", () => { + expect(isValidVirtualHost(undefined)).toBeFalsy() + expect(isValidVirtualHost("github.com")).toBeFalsy() + expect(isValidVirtualHost("http://github.com")).toBeFalsy() + expect(isValidVirtualHost("github.com/")).toBeFalsy() + + expect(isValidVirtualHost("github.com/sheepzh")).toBeTruthy() + expect(isValidVirtualHost("github.com/**")).toBeTruthy() + expect(isValidVirtualHost("github.com/*")).toBeTruthy() + expect(isValidVirtualHost("github.com/*/timer")).toBeTruthy() + // Can't end with / + expect(isValidVirtualHost("github.com/*/timer/")).toBeFalsy() }) \ No newline at end of file diff --git a/types/chrome.d.ts b/types/chrome.d.ts new file mode 100644 index 000000000..c78e03acd --- /dev/null +++ b/types/chrome.d.ts @@ -0,0 +1,18 @@ +/** + * ABBRs for namespace chrome + */ +// chrome.tabs +declare type ChromeTab = chrome.tabs.Tab +declare type ChromeTabActiveInfo = chrome.tabs.TabActiveInfo +declare type ChromeTabChangeInfo = chrome.tabs.TabChangeInfo +// chrome.windows +declare type ChromeWindow = chrome.windows.Window +// chrome.contextMenus +declare type ChromeContextMenuCreateProps = chrome.contextMenus.CreateProperties +declare type ChromeContextMenuUpdateProps = chrome.contextMenus.UpdateProperties +// chrome.alarms +declare type ChromeAlarm = chrome.alarms.Alarm +// chrome.runtime +declare type ChromeOnInstalledReason = chrome.runtime.OnInstalledReason +declare type ChromeMessageSender = chrome.runtime.MessageSender +declare type ChromeMessageHandler = (req: timer.mq.Request, sender: ChromeMessageSender) => Promise> \ No newline at end of file diff --git a/types/common.d.ts b/types/common.d.ts new file mode 100644 index 000000000..7e6640f4e --- /dev/null +++ b/types/common.d.ts @@ -0,0 +1,8 @@ +// Embedded partial +declare type EmbeddedPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : EmbeddedPartial; +} diff --git a/types/json.d.ts b/types/json.d.ts deleted file mode 100644 index 49611ecdf..000000000 --- a/types/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.json" { - const value: any - export default value -} \ No newline at end of file diff --git a/types/timer/app.d.ts b/types/timer/app.d.ts new file mode 100644 index 000000000..f7b85c721 --- /dev/null +++ b/types/timer/app.d.ts @@ -0,0 +1,10 @@ +declare namespace timer.app { + /** + * @since 1.1.7 + */ + type TimeFormat = + | "default" + | "second" + | "minute" + | "hour" +} \ No newline at end of file diff --git a/types/timer/backup.d.ts b/types/timer/backup.d.ts new file mode 100644 index 000000000..d4bf195cb --- /dev/null +++ b/types/timer/backup.d.ts @@ -0,0 +1,32 @@ +/** + * @since 1.2.0 + */ +declare namespace timer.backup { + + type Type = + | 'none' + | 'gist' + + /** + * Snapshot of last backup + */ + type Snapshot = { + /** + * Timestamp + */ + ts: number + /** + * The date of the ts + */ + date: string + } + + /** + * Snapshot cache + */ + type SnaptshotCache = Partial<{ + [type in Type]: Snapshot + }> + + type MetaCache = Partial> +} \ No newline at end of file diff --git a/types/timer/common.d.ts b/types/timer/common.d.ts new file mode 100644 index 000000000..e0f02c630 --- /dev/null +++ b/types/timer/common.d.ts @@ -0,0 +1,15 @@ +declare namespace timer.common { + type Pagination = { + size: number + num: number + total: number + } + type PageQuery = { + num?: number + size?: number + } + type PageResult = { + list: T[] + total: number + } +} \ No newline at end of file diff --git a/types/timer/index.d.ts b/types/timer/index.d.ts new file mode 100644 index 000000000..1809c54bd --- /dev/null +++ b/types/timer/index.d.ts @@ -0,0 +1,71 @@ +declare namespace timer { + /** + * The source locale + * + * @since 1.4.0 + */ + type SourceLocale = 'en' + /** + * The locale must be translated with code + * + * @since 1.5.3 + */ + type RequiredLocale = SourceLocale | 'zh_CN' + type OptionalLocale = + | 'ja' + // @since 0.9.0 + | 'zh_TW' + /** + * @since 0.8.0 + */ + type Locale = RequiredLocale | OptionalLocale + + /** + * Translating locales + * + * @since 1.4.0 + */ + type TranslatingLocale = + | 'de' + | 'es' + | 'ko' + | 'pl' + | 'pt' + | 'pt_BR' + | 'ru' + | 'uk' + | 'fr' + | 'it' + | 'sv' + | 'fi' + | 'da' + | 'hr' + | 'id' + | 'tr' + | 'cs' + | 'ro' + | 'nl' + | 'vi' + | 'sk' + | 'mn' + + type ExtensionMeta = { + installTime?: number + appCounter?: { [routePath: string]: number } + popupCounter?: { + _total?: number + } + /** + * The id of this client + * + * @since 1.2.0 + */ + cid?: string + backup?: { + [key in timer.backup.Type]?: { + ts: number + msg?: string + } + } + } +} \ No newline at end of file diff --git a/types/timer/limit.d.ts b/types/timer/limit.d.ts new file mode 100644 index 000000000..3d5b79d2e --- /dev/null +++ b/types/timer/limit.d.ts @@ -0,0 +1,47 @@ +declare namespace timer.limit { + /** + * Limit rule in runtime + * + * @since 0.8.4 + */ + type Item = Rule & { + regular: RegExp + /** + * Waste today, milliseconds + */ + waste?: number + } + type Rule = { + /** + * Condition, can be regular expression with star signs + */ + cond: string + /** + * Time limit, seconds + */ + time: number + enabled: boolean + /** + * Allow to delay 5 minutes if time over + */ + allowDelay: boolean + } + type Record = Rule & { + /** + * The latest record date + */ + latestDate: string + /** + * Time wasted in the latest record date + */ + wasteTime: number + } + /** + * @since 1.3.2 + */ + type FilterType = + // translucent filter + | 'translucent' + // ground glass filter + | 'groundGlass' +} diff --git a/types/timer/merge.d.ts b/types/timer/merge.d.ts new file mode 100644 index 000000000..753e562c0 --- /dev/null +++ b/types/timer/merge.d.ts @@ -0,0 +1,18 @@ +declare namespace timer.merge { + type Rule = { + /** + * Origin host, can be regular expression with star signs + */ + origin: string + /** + * The merge result + * + * + Empty string means equals to the origin host + * + Number means the count of kept dots, must be natural number (int & >=0) + */ + merged: string | number + } + interface Merger { + merge(host: string): string + } +} diff --git a/types/timer/mq.d.ts b/types/timer/mq.d.ts new file mode 100644 index 000000000..1ad8d738f --- /dev/null +++ b/types/timer/mq.d.ts @@ -0,0 +1,45 @@ +/** +* Message queue +*/ +declare namespace timer.mq { + type ReqCode = + | 'openLimitPage' + | 'limitTimeMeet' + // @since 0.9.0 + | 'limitWaking' + // @since 1.2.3 + | 'limitChanged' + // Request by content script + // @since 1.3.0 + | "cs.isInWhitelist" + | "cs.incVisitCount" + | "cs.printTodayInfo" + | "cs.getTodayInfo" + | "cs.moreMinutes" + | "cs.getLimitedRules" + type ResCode = "success" | "fail" | "ignore" + + /** + * @since 0.2.2 + */ + type Request = { + code: ReqCode + data: T + } + /** + * @since 0.8.4 + */ + type Response = { + code: ResCode, + msg?: string + data?: T + } + /** + * @since 1.3.0 + */ + type Handler = (data: Req, sender: chrome.runtime.MessageSender) => Promise + /** + * @since 0.8.4 + */ + type Callback = (result?: Response) => void +} \ No newline at end of file diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts new file mode 100644 index 000000000..5e78e9552 --- /dev/null +++ b/types/timer/option.d.ts @@ -0,0 +1,161 @@ +/** + * The options + * + * @since 0.3.0 + */ +declare namespace timer.option { + type PopupDuration = + | "today" | "thisWeek" | "thisMonth" + | "last30Days" + /** + * Options used for the popup page + */ + type PopupOption = { + /** + * The max count of today's data to display in popup page + */ + popupMax: number + /** + * The default type to display + */ + defaultType: stat.Dimension + /** + * The default duration to search + * @since 0.6.0 + */ + defaultDuration: PopupDuration + /** + * Replace the host name with site name which is detected automatically from the title of site homepages, + * or modified manually by the user + * + * @since 0.5.0 + */ + displaySiteName: boolean + /** + * The start of one week + * + * @since 1.2.5 + */ + weekStart: WeekStartOption + /** + * Whether to merge domain by default + * + * @since 1.3.2 + */ + defaultMergeDomain: boolean + } + + /** + * @since 1.2.5 + */ + type WeekStartOption = + | 'default' + | number // Weekday, From 1 to 7 + + type DarkMode = + // Follow the OS, @since 1.3.3 + | "default" + // Always on + | "on" + // Always off + | "off" + // Timed on + | "timed" + + type AppearanceOption = { + /** + * Whether to display the whitelist button in the context menu + * + * @since 0.3.2 + */ + displayWhitelistMenu: boolean + /** + * Whether to display the badge text of focus time + * + * @since 0.3.3 + */ + displayBadgeText: boolean + /** + * The language of this extension + * + * @since 0.8.0 + */ + locale: LocaleOption + /** + * Whether to print the info in the console + * + * @since 0.8.6 + */ + printInConsole: boolean + /** + * The state of dark mode + * + * @since 1.1.0 + */ + darkMode: DarkMode + /** + * The range of seconds to turn on dark mode. Required if {@param darkMode} is 'timed' + * + * @since 1.1.0 + */ + darkModeTimeStart?: number + darkModeTimeEnd?: number + /** + * The filter of limit mark + * @since 1.3.2 + */ + limitMarkFilter: limit.FilterType + } + + type StatisticsOption = { + /** + * Count when idle + */ + countWhenIdle: boolean + /** + * Whether to collect the site name + * + * @since 0.5.0 + */ + collectSiteName: boolean + /** + * Whether to count the local files + * @since 0.7.0 + */ + countLocalFiles: boolean + } + + /** + * The options of backup + * + * @since 1.2.0 + */ + type BackupOption = { + /** + * The type 2 backup + */ + backupType: backup.Type + /** + * The auth of types, maybe ak/sk or static token + */ + backupAuths: { [type in backup.Type]?: string } + /** + * The name of this client + */ + clientName: string + /** + * Whether to auto-backup data + */ + autoBackUp: boolean + /** + * Interval to auto-backup data, minutes + */ + autoBackUpInterval: number + } + + type AllOption = PopupOption & AppearanceOption & StatisticsOption & BackupOption + /** + * @since 0.8.0 + */ + type LocaleOption = Locale | "default" +} diff --git a/types/timer/period.d.ts b/types/timer/period.d.ts new file mode 100644 index 000000000..5ee2f0dd7 --- /dev/null +++ b/types/timer/period.d.ts @@ -0,0 +1,32 @@ +declare namespace timer.period { + type Key = { + year: number + month: number + date: number + /** + * 0~95 + * ps. 95 = 60 / 15 * 24 - 1 + */ + order: number + } + type Result = Key & { + /** + * 1~900000 + * ps. 900000 = 15min * 60s/min * 1000ms/s + */ + milliseconds: number + } + type Row = { + /** + * {yyyy}{mm}{dd} + */ + date: string + startTime: Date + endTime: Date + /** + * 1 - 60000 + * ps. 60000 = 60s * 1000ms/s + */ + milliseconds: number + } +} \ No newline at end of file diff --git a/types/timer/site.d.ts b/types/timer/site.d.ts new file mode 100644 index 000000000..f99cb9761 --- /dev/null +++ b/types/timer/site.d.ts @@ -0,0 +1,29 @@ +declare namespace timer.site { + /** + * @since 0.5.0 + */ + type AliasSource = + | 'USER' // By user + | 'DETECTED' // Auto-detected + type SiteKey = { + host: string + /** + * @since 1.2.1 + */ + merged?: boolean + /** + * Whether visual site + * + * @since 1.6.0 + */ + virtual?: boolean + } + type SiteInfo = SiteKey & { + alias?: string + /** + * The source of name + */ + source?: AliasSource + iconUrl?: string + } +} \ No newline at end of file diff --git a/types/timer/stat.d.ts b/types/timer/stat.d.ts new file mode 100644 index 000000000..327b7ab21 --- /dev/null +++ b/types/timer/stat.d.ts @@ -0,0 +1,101 @@ +declare namespace timer.stat { + /** + * The dimension to statistics + */ + type Dimension = + // Focus time + | 'focus' + // Visit count + | 'time' + + /** + * The stat result of host + * + * @since 0.0.1 + */ + type Result = { + [item in Dimension]: number + } + + /** + * A set of results + * + * @since 0.3.3 + */ + type ResultSet = { [host: string]: Result } + + /** + * The unique key of each data row + */ + type RowKey = { + host: string + // Absent if date merged + date?: string + } + + type RowBase = RowKey & Result + + /** + * Row of each statistics result + */ + type Row = RowBase & { + /** + * The merged domains + * + * Can't be empty if merged + * + * @since 0.1.5 + */ + mergedHosts: Row[] + /** + * Whether virtual host + * + * @since 1.6.0 + */ + virtual: boolean + /** + * The composition of data when querying remote + */ + composition?: RemoteComposition + /** + * Icon url + * + * Must be undefined if merged + */ + iconUrl?: string + /** + * The alias name of this Site, always is the title of its homepage by detected + */ + alias?: string + /** + * The id of client where the remote data is storaged + */ + cid?: string + /** + * The name of client where the remote data is storaged + */ + cname?: string + } + + type RemoteCompositionVal = + // Means local data + number | { + /** + * Client's id + */ + cid: string + /** + * Client's name + */ + cname?: string + value: number + } + + /** + * @since 1.4.7 + */ + type RemoteComposition = { + [item in Dimension]: RemoteCompositionVal[] + } + +} \ No newline at end of file diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index cc8af3f96..2dc5af0c0 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -79,7 +79,7 @@ const staticOptions: webpack.Configuration = { ] }, resolve: { - extensions: ['.ts', ".js", '.css', '.scss', '.sass'], + extensions: ['.ts', '.tsx', ".js", '.css', '.scss', '.sass'], alias: resolveAlias, fallback: { // fallbacks of axios's dependencies start diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index baa398ba9..8b6a4d1f1 100644 --- a/webpack/webpack.prod.ts +++ b/webpack/webpack.prod.ts @@ -15,7 +15,7 @@ const sourceCodeForFireFox = path.resolve(__dirname, '..', 'market_packages', `$ // Temporary directory for source code to archive on Firefox const sourceTempDir = path.resolve(__dirname, '..', 'firefox') -const srcDir = ['public', 'src', "test", 'package.json', 'tsconfig.json', 'webpack', 'global.d.ts', "jest.config.ts", "script", ".gitignore"] +const srcDir = ['public', 'src', "test", "types", 'package.json', 'tsconfig.json', 'webpack', "jest.config.ts", "script", ".gitignore"] const copyMapper = srcDir.map(p => { return { source: path.resolve(__dirname, '..', p), destination: path.resolve(sourceTempDir, p) } }) const readmeForFirefox = path.join(__dirname, '..', 'doc', 'for-fire-fox.md')