From 6730f021b95845cfe1455ffe89f52a9e4a33d624 Mon Sep 17 00:00:00 2001 From: ZHY Date: Sat, 15 Oct 2022 22:02:59 +0800 Subject: [PATCH 001/168] Update for Safari --- doc/dev-guide.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/dev-guide.md b/doc/dev-guide.md index 447dc0472..67a577965 100644 --- a/doc/dev-guide.md +++ b/doc/dev-guide.md @@ -53,11 +53,25 @@ yarn run test ``` 7. 提交代码,并 PR 主仓库的 main 分支 -## 3. 应用架构设计 +## 3. 在 Safari 里运行 + +1. 重复上述步骤 1-4 +2. 编译兼容 Safari 的代码,替换上述步骤 5 的指令即可 +```shell +npm run dev:safari +``` +3. 使用 Xcode 内置工具 safari-web-extension-converter 将 Chrome 扩展转换成 Safari 扩展 +```shell +[YOUR_PATH]/Xcode.app/Contents/Developer/usr/bin/safari-web-extension-converter dist_dev_safari +``` +项目根目录下会生成一个文件夹 Timer_Safari_DEV,同时 Xcode 会自动打开该文件夹 +4. 在 Xcode 里运行打开的项目即可 + +## 4. 应用架构设计 > todo -## 4. 目录结构 +## 5. 目录结构 ```plain project @@ -117,6 +131,7 @@ project └───webpack # webpack 打包配置 | webpack.common.ts # 基础配置 | webpack.dev.ts # 开发环境配置 + | webpack.dev.safari.ts # Safari 开发环境配置 | webpack.prod.ts # 生产配置 ``` From 945e6ffa088fce679151938b66445d3039bdb703 Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 19:38:01 +0800 Subject: [PATCH 002/168] Create safari-install.md --- safari-install.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 safari-install.md diff --git a/safari-install.md b/safari-install.md new file mode 100644 index 000000000..4d6b2497b --- /dev/null +++ b/safari-install.md @@ -0,0 +1,43 @@ +# How to install in Safari + +This is a too poor developer to pay $99 per year for distribution of an opensource and free browser extension in Apple App Store. +So please intall it **manually**, GG Safari. + +## 0. Download this repository + +```shell +git clone https://github.com/sheepzh/timer.git +cd timer +``` + +## 1. Install tools + +Some tools are required to compile this project to an executable software for Safari. + +* [nodejs & npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) +* [Xcode (compitable for your version of macOS)](https://developer.apple.com/xcode/) + +## 2. Compile source code, install & run + +There are serveral steps. + +1. Compile the sourcecode programmed with TypeScript to js bundles. + +```shell +# Install dependencies +npm install +# Compile +npm run build:safari +``` +Then there will be one folder called **Timer**. + +Also, you can download the archived file from [the release page](https://github.com/sheepzh/timer/releases), and unzip it to gain this folder. + +2. Convert js bundles to Xcode project + +```shell +[YOUR_PATH]/Xcode.app/Contents/Developer/usr/bin/safari-web-extension-converter ./Timer +``` +3. Run Xcode project and one extension app will installed on your macOS +4. Enable this extension +5. Finnally, open your Safari From a1496497887ea9e5494ca282aa886cc9d8749e2d Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 19:45:20 +0800 Subject: [PATCH 003/168] Ignore output path of Safari (#152) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 461018b64..25488f8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ dist dist_dev dist_dev_safari dist_prod +dist_prod_safari dist_analyze Timer_Safari_DEV +Timer firefox_dev From 153b77bb548cc6a2d3d85218cf2e4b5e8c4f5e52 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 18 Oct 2022 19:48:14 +0800 Subject: [PATCH 004/168] Not display icons of website in Safari (#152) --- src/app/components/common/host-alert.ts | 36 ++++++++++------- src/app/components/report/index.ts | 3 +- src/background/icon-and-alias-collector.ts | 6 +-- src/popup/components/chart/option.ts | 6 ++- src/popup/components/footer/index.ts | 3 +- webpack/webpack.prod.safari.ts | 47 ++++++++++++++++++++++ 6 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 webpack/webpack.prod.safari.ts diff --git a/src/app/components/common/host-alert.ts b/src/app/components/common/host-alert.ts index 4c5d85465..14b43e428 100644 --- a/src/app/components/common/host-alert.ts +++ b/src/app/components/common/host-alert.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { IS_SAFARI } from "@util/constant/environment" import { ElLink } from "element-plus" import { computed, ComputedRef, defineComponent, h } from "vue" @@ -39,20 +40,27 @@ const _default = defineComponent({ const href: ComputedRef = computed(() => props.clickable ? `http://${props.host}` : '') const target: ComputedRef = computed(() => props.clickable ? '_blank' : '') const cursor: ComputedRef = computed(() => props.clickable ? "cursor" : "default") - return () => h('div', [ - h(ElLink, - { - href: href.value, - target: target.value, - underline: props.clickable, - style: { cursor: cursor.value } - }, - () => props.host - ), h('span', - { style: HOST_ICON_STYLE }, - h('img', { src: props.iconUrl, width: 12, height: 12 }) - ) - ]) + return IS_SAFARI + ? () => h(ElLink, { + href: href.value, + target: target.value, + underline: props.clickable, + style: { cursor: cursor.value } + }, () => props.host) + : () => h('div', [ + h(ElLink, + { + href: href.value, + target: target.value, + underline: props.clickable, + style: { cursor: cursor.value } + }, + () => props.host + ), h('span', + { style: HOST_ICON_STYLE }, + h('img', { src: props.iconUrl, width: 12, height: 12 }) + ) + ]) } }) diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index 78d5daf6d..6ad143e7f 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -27,6 +27,7 @@ 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" const timerDatabase = new TimerDatabase(chrome.storage.local) @@ -38,7 +39,7 @@ async function queryData( ) { const loading = ElLoadingService({ target: `.container-card>.el-card__body`, text: "LOADING..." }) const pageInfo = { size: page.size, num: page.num } - const fillFlag = { alias: true, iconUrl: true } + const fillFlag = { alias: true, iconUrl: !IS_SAFARI } const param = { ...queryParam.value, inclusiveRemote: readRemote.value diff --git a/src/background/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index adeec6e2c..499ceae66 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -8,7 +8,7 @@ import HostAliasDatabase from "@db/host-alias-database" import IconUrlDatabase from "@db/icon-url-database" import OptionDatabase from "@db/option-database" -import { IS_CHROME } from "@util/constant/environment" +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" @@ -51,7 +51,7 @@ async function processTabInfo(tab: chrome.tabs.Tab): Promise { // localhost hosts with Chrome use cache, so keep the favIcon url undefined IS_CHROME && /^localhost(:.+)?/.test(host) && (favIconUrl = undefined) const iconUrl = favIconUrl || iconUrlOfBrowser(protocol, host) - iconUrlDatabase.put(host, iconUrl) + iconUrl && iconUrlDatabase.put(host, iconUrl) collectAliasEnabled && !isBrowserUrl(url) && isHomepage(url) && collectAlias(host, tab.title) } @@ -67,7 +67,7 @@ function handleWebNavigationCompleted(detail: chrome.webNavigation.WebNavigation } function listen() { - chrome.webNavigation.onCompleted.addListener(handleWebNavigationCompleted) + !IS_SAFARI && chrome.webNavigation.onCompleted.addListener(handleWebNavigationCompleted) } /** diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index a6496e2e4..7bbb77a15 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -18,6 +18,7 @@ import { formatPeriodCommon, formatTime } from "@util/time" import { t } from "@popup/locale" import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" import { generateSiteLabel } from "@util/site" +import { IS_SAFARI } from "@util/constant/environment" type EcOption = ComposeOption< | PieSeriesOption @@ -75,7 +76,10 @@ function labelFormatter({ mergeHost }: timer.popup.QueryResult, params: any): st const format = params instanceof Array ? params[0] : params const { name } = format const data = format.data as timer.popup.Row - return mergeHost || data.isOther ? name : `{${legend2LabelStyle(name)}|} {a|${name}}` + // Un-supported to get favicon url in Safari + return mergeHost || data.isOther || IS_SAFARI + ? name + : `{${legend2LabelStyle(name)}|} {a|${name}}` } const staticOptions: EcOption = { diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 7f15936c4..dc79065fd 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -20,12 +20,13 @@ import { t } from "@popup/locale" import { locale } from "@util/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 & { chartTitle: string } -const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: true, alias: true } +const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: !IS_SAFARI, alias: true } function calculateDateRange(duration: timer.popup.Duration, weekStart: timer.option.WeekStartOption): Date | Date[] { const now = new Date() diff --git a/webpack/webpack.prod.safari.ts b/webpack/webpack.prod.safari.ts new file mode 100644 index 000000000..b3ab5cd58 --- /dev/null +++ b/webpack/webpack.prod.safari.ts @@ -0,0 +1,47 @@ +import path from "path" +import optionGenerator from "./webpack.common" +import FileManagerWebpackPlugin from "filemanager-webpack-plugin" +import webpack from "webpack" + +const { name, version } = require(path.join(__dirname, '..', 'package.json')) + +const outputDir = path.join(__dirname, '..', 'dist_prod_safari') +const normalZipFilePath = path.resolve(__dirname, '..', 'market_packages', `${name}-${version}-safari.zip`) + +function removeUnsupportedProperties(manifest: Partial) { + // 1. permissions. 'idle' is not supported + const originPermissions = manifest.permissions || [] + const unsupported = ['idle'] + const supported = [] + originPermissions.forEach(perm => !unsupported.includes(perm) && supported.push(perm)) + manifest.permissions = supported +} + +const options = optionGenerator( + outputDir, + baseManifest => { + baseManifest.name = 'Timer' + // Remove unsupported properties in Safari + removeUnsupportedProperties(baseManifest) + } +) + +const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ + events: { + // Archive at the end + onEnd: [ + { delete: [path.join(outputDir, '*.LICENSE.txt')] }, + // Define plugin to archive zip for different markets + { + delete: [normalZipFilePath], + archive: [{ source: outputDir, destination: normalZipFilePath }] + } + ] + } +}) + +options.mode = 'production' +options.plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +options.output.path = outputDir + +export default options \ No newline at end of file From 06adecb1682a65af69860661e81d49a75cd2596a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 18 Oct 2022 20:23:22 +0800 Subject: [PATCH 005/168] Move --- safari-install.md => doc/safari-install.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename safari-install.md => doc/safari-install.md (100%) diff --git a/safari-install.md b/doc/safari-install.md similarity index 100% rename from safari-install.md rename to doc/safari-install.md From b163024b328d69c3002cc6efc77d6caaca58a86f Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 20:26:02 +0800 Subject: [PATCH 006/168] Update safari-install.md --- doc/safari-install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/safari-install.md b/doc/safari-install.md index 4d6b2497b..03c18cf6b 100644 --- a/doc/safari-install.md +++ b/doc/safari-install.md @@ -1,4 +1,4 @@ -# How to install in Safari +# How to install for Safari This is a too poor developer to pay $99 per year for distribution of an opensource and free browser extension in Apple App Store. So please intall it **manually**, GG Safari. From 1b2d10fcbb8475d93780e3d710e7ab444203c514 Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 20:26:32 +0800 Subject: [PATCH 007/168] Add link for Safari --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 213cfe0c1..81c428041 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Timer can [![](https://img.shields.io/amo/rating/2690100?color=green)](https://addons.mozilla.org/en-US/firefox/addon/2690100) [![Mozilla Add-on](https://img.shields.io/amo/users/2690100?color=green)](https://addons.mozilla.org/en-US/firefox/addon/2690100) +[How to install manually for Safari](./doc/safari-install.md) + ![User Count](https://gist.githubusercontent.com/sheepzh/6aaf4c22f909db73b533491167da129b/raw/user_count.svg) ## Contribution From 80b073751b831c162663a4ed0b5592b78a8349ce Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 20:36:24 +0800 Subject: [PATCH 008/168] Add command to build for Safari in prodction mode --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a7f7c9983..44b7d9034 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "webpack --config=webpack/webpack.dev.ts --watch", "dev:safari": "webpack --config=webpack/webpack.dev.safari.ts --watch", "build": "webpack --config=webpack/webpack.prod.ts", + "build:safari": "webpack --config=webpack/webpack.prod.safari.ts", "analyze": "webpack --config=webpack/webpack.analyze.ts", "test": "jest --env=jsdom", "test-c": "jest --coverage --env=jsdom" @@ -57,4 +58,4 @@ "vue": "^3.2.37", "vue-router": "^4.0.16" } -} \ No newline at end of file +} From 710936693fe9b7802e2eef1a12821083ca1087cd Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 20:41:20 +0800 Subject: [PATCH 009/168] v1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44b7d9034..7542b3450 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.2.7", + "version": "1.3.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From a3d4d81213830bdb8f0396aeb36f378d4de637be Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 18 Oct 2022 20:54:07 +0800 Subject: [PATCH 010/168] Fix i18n error (#150) --- src/popup/components/chart/option.ts | 61 ++++++++++++++-------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index a8750f67c..c1d2497a9 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -103,36 +103,7 @@ const staticOptions: EcOption = { shadowColor: "rgba(0, 0, 0, 0.5)", }, } - }], - toolbox: { - show: true, - feature: { - restore: { - show: true, - title: t(msg => msg.restoreTitle) - }, - saveAsImage: { - show: true, - title: t(msg => msg.saveAsImageTitle), - // file name - name: t(msg => msg.fileName, { - app: t(msg => msg.appName), - today - }), - excludeComponents: ['toolbox'], - pixelRatio: 1 - }, - // Customized tool's name must start with 'my' - myOptions: { - show: true, - title: t(msg => msg.options), - icon: optionIcon, - onclick() { - chrome.tabs.create({ url: getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' }) }) - } - } - } - } + }] } const maxWidth = 750 @@ -210,7 +181,35 @@ export function pieOptions(props: timer.popup.ChartProps, container: HTMLDivElem color: textColor } }], - toolbox: staticOptions.toolbox + toolbox: { + show: true, + feature: { + restore: { + show: true, + title: t(msg => msg.restoreTitle) + }, + saveAsImage: { + show: true, + title: t(msg => msg.saveAsImageTitle), + // file name + name: t(msg => msg.fileName, { + app: t(msg => msg.appName), + today + }), + excludeComponents: ['toolbox'], + pixelRatio: 1 + }, + // Customized tool's name must start with 'my' + myOptions: { + show: true, + title: t(msg => msg.options), + icon: optionIcon, + onclick() { + chrome.tabs.create({ url: getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' }) }) + } + } + } + } } const series = [] const iconRich = {} From 43a4d531deb8ddc17e8f2b64f67c93f1d67edc10 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 20 Oct 2022 18:28:12 +0800 Subject: [PATCH 011/168] Remove archivement function (#141) --- .../1-3-0/archived-data-cleaner.ts | 28 +++++++++++++++++++ .../version-manager/i-version-processor.ts | 2 +- src/background/version-manager/index.ts | 7 ++++- src/database/archived-database.ts | 8 ++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/background/version-manager/1-3-0/archived-data-cleaner.ts diff --git a/src/background/version-manager/1-3-0/archived-data-cleaner.ts b/src/background/version-manager/1-3-0/archived-data-cleaner.ts new file mode 100644 index 000000000..c62df8441 --- /dev/null +++ b/src/background/version-manager/1-3-0/archived-data-cleaner.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2022-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import ArchivedDatabase from "@db/archived-database" +import { log } from "@src/common/logger" +import IVersionProcessor from "../i-version-processor" + +/** + * All code will be removed at v1.4.0 + */ +export default class ArchivedDataCleaner implements IVersionProcessor { + since(): string { + return '1.3.0' + } + + async process(reason: chrome.runtime.OnInstalledReason): Promise { + const db = new ArchivedDatabase(chrome.storage.local) + if (reason !== 'update') { + return + } + const count = await db.removeAll() + log(`Removed ${count} archived items`) + } +} \ No newline at end of file diff --git a/src/background/version-manager/i-version-processor.ts b/src/background/version-manager/i-version-processor.ts index fb6614f9a..e9e8b0bb4 100644 --- a/src/background/version-manager/i-version-processor.ts +++ b/src/background/version-manager/i-version-processor.ts @@ -21,5 +21,5 @@ export default interface IVersionProcessor { * * @param reason reason of chrome OnInstalled event */ - process(reason: string): void + process(reason: chrome.runtime.OnInstalledReason): void } \ No newline at end of file diff --git a/src/background/version-manager/index.ts b/src/background/version-manager/index.ts index 6c14e91ce..b0d4a477a 100644 --- a/src/background/version-manager/index.ts +++ b/src/background/version-manager/index.ts @@ -8,6 +8,7 @@ import IVersionProcessor from "./i-version-processor" import HostMergeInitializer from "./0-1-2/host-merge-initializer" import LocalFileInitializer from "./0-7-0/local-file-initializer" +import ArchivedDataCleaner from "./1-3-0/archived-data-cleaner" /** * Version manager @@ -18,7 +19,11 @@ class VersionManager { processorChain: IVersionProcessor[] = [] constructor() { - this.processorChain.push(new HostMergeInitializer(), new LocalFileInitializer()) + this.processorChain.push( + new HostMergeInitializer(), + new LocalFileInitializer(), + new ArchivedDataCleaner(), + ) this.processorChain = this.processorChain.sort((a, b) => a.since() >= b.since() ? 1 : 0) } diff --git a/src/database/archived-database.ts b/src/database/archived-database.ts index d55746fb1..056392e52 100644 --- a/src/database/archived-database.ts +++ b/src/database/archived-database.ts @@ -28,6 +28,14 @@ class ArchivedDatabase extends BaseDatabase { return Promise.resolve(result) } + async removeAll(): Promise { + const items = await this.storage.get() + const keys = Object.keys(items) + .filter(key => key.startsWith(ARCHIVED_PREFIX)) + await this.storage.remove(keys) + return keys?.length || 0 + } + private generateKey(row: timer.stat.Row): string { return ARCHIVED_PREFIX + row.host } From d78760429385530b3dbd28f5082b1982a3a65ade Mon Sep 17 00:00:00 2001 From: ZHY Date: Sun, 23 Oct 2022 17:26:53 +0800 Subject: [PATCH 012/168] Update year --- LICENSE_ANTI_996 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE_ANTI_996 b/LICENSE_ANTI_996 index 3216b6868..84806beb1 100644 --- a/LICENSE_ANTI_996 +++ b/LICENSE_ANTI_996 @@ -1,4 +1,4 @@ -Copyright (c) +Copyright (c) 2021 Hengyang Zhang "Anti 996" License Version 1.0 (Draft) From edb2c07bc296f4a089954aca295b711b81f6070b Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 17:33:16 +0800 Subject: [PATCH 013/168] Remove donation provided by mianbaoduo.com (#155) --- src/app/layout/menu.ts | 11 ++--------- src/popup/components/footer/index.ts | 2 -- src/popup/components/footer/meat.ts | 21 --------------------- src/util/constant/url.ts | 5 ----- 4 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 src/popup/components/footer/meat.ts diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 5e1984c9d..6bf3b108e 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -14,7 +14,7 @@ import { defineComponent, h, onMounted, reactive } from "vue" import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" -import { HOME_PAGE, MEAT_URL, TRANSLATION_ISSUE_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" +import { HOME_PAGE, TRANSLATION_ISSUE_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" import { Aim, Calendar, ChatSquare, Folder, Food, HotWater, MagicStick, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer, TrendCharts } from "@element-plus/icons-vue" import { locale } from "@util/i18n" import TrendIcon from "./icon/trend-icon" @@ -62,14 +62,7 @@ function generateMenus(): _MenuGroup[] { icon: ChatSquare, index: '_feedback' }) - if (isZhCn) { - otherMenuItems.push({ - title: 'meat', - href: MEAT_URL, - icon: Food, - index: '_meat' - }) - } else { + if (!isZhCn) { otherMenuItems.push({ title: 'translationMistake', href: TRANSLATION_ISSUE_PAGE, diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index dc79065fd..bbdd020b4 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -9,7 +9,6 @@ import type { FillFlagParam, TimerQueryParam } from "@service/timer-service" import initAllFunction from './all-function' import initUpgrade from './upgrade' -import initMeat from './meat' import TotalInfoWrapper from "./total-info" import MergeHostWrapper from "./merge-host" import TimeSelectWrapper from "./select/time-select" @@ -73,7 +72,6 @@ class FooterWrapper { async init() { initAllFunction() initUpgrade() - initMeat() const query = () => this.query() diff --git a/src/popup/components/footer/meat.ts b/src/popup/components/footer/meat.ts deleted file mode 100644 index 74fe56ed3..000000000 --- a/src/popup/components/footer/meat.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { MEAT_URL } from "@util/constant/url" -import { locale } from "@util/i18n" - -function initMeat() { - const nowHour = new Date().getHours() - - if ((nowHour === 17 || nowHour === 18 || nowHour === 12) && locale === "zh_CN") { - const link = document.getElementById('meat-container') - link.style.display = 'block' - link.onclick = () => chrome.tabs.create({ url: MEAT_URL }) - } -} - -export default initMeat \ No newline at end of file diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 02b8fd152..9947f1a6c 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -103,11 +103,6 @@ export function iconUrlOfBrowser(protocol: string, host: string): string { } else return '' } -/** - * @since 0.2.9 - */ -export const MEAT_URL = 'https://dun.mianbaoduo.com/@yangyang' - /** * @since 0.9.3 */ From ed75d3ca3e36c2c40e02bfef9f9122a7acfd478c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 17:34:06 +0800 Subject: [PATCH 014/168] Clean code --- src/app/layout/menu.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 6bf3b108e..8d77cb596 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -4,7 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { ComponentOptionsMixin, DefineComponent, RendererElement, RendererNode, UnwrapRef, VNode } from "vue" +import type { UnwrapRef } from "vue" import type ElementIcon from "../element-ui/icon" import type { RouteLocationNormalizedLoaded, Router } from "vue-router" import type { I18nKey } from "@app/locale" @@ -15,7 +15,7 @@ import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" import { HOME_PAGE, TRANSLATION_ISSUE_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" -import { Aim, Calendar, ChatSquare, Folder, Food, HotWater, MagicStick, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer, TrendCharts } from "@element-plus/icons-vue" +import { Aim, Calendar, ChatSquare, Folder, HotWater, MagicStick, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" import { locale } from "@util/i18n" import TrendIcon from "./icon/trend-icon" From 41432984dbac6e58026f9b210daa20024a40c877 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 17:45:29 +0800 Subject: [PATCH 015/168] Fix translations --- src/util/i18n/components/item.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/i18n/components/item.ts b/src/util/i18n/components/item.ts index ad0cac04b..749463bb2 100644 --- a/src/util/i18n/components/item.ts +++ b/src/util/i18n/components/item.ts @@ -54,7 +54,7 @@ const _default: Messages = { host: '域名', total: '運行時長', focus: '瀏覽時長', - time: '打開次數', + time: '訪問次數', operation: { label: '操作', delete: '刪除', From 8822aa76d696f892e4acafe0ee48f870e222cdd2 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 17:51:04 +0800 Subject: [PATCH 016/168] Upgrade jest to v29 --- package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7542b3450..640856a80 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,11 @@ "author": "zhy", "license": "MIT", "devDependencies": { - "@jest/types": "^28.1.1", "@types/chrome": "0.0.190", "@types/copy-webpack-plugin": "^8.0.1", "@types/echarts": "^4.9.15", "@types/generate-json-webpack-plugin": "^0.3.4", - "@types/jest": "^28.1.2", + "@types/jest": "^29.2.0", "@types/node": "^18.0.0", "@types/psl": "^1.1.0", "@types/webpack": "^5.28.0", @@ -31,13 +30,13 @@ "eslint": "^8.18.0", "filemanager-webpack-plugin": "^7.0.0", "generate-json-webpack-plugin": "^2.0.0", - "jest": "^28.1.1", - "jest-environment-jsdom": "^28.1.1", + "jest": "^29.2.1", + "jest-environment-jsdom": "^29.2.1", "mini-css-extract-plugin": "^2.6.1", "node-sass": "^7.0.1", "sass-loader": "^13.0.0", "style-loader": "^3.3.1", - "ts-jest": "^28.0.5", + "ts-jest": "^29.0.3", "ts-loader": "^9.3.0", "ts-node": "^10.8.1", "tslib": "^2.4.0", @@ -58,4 +57,4 @@ "vue": "^3.2.37", "vue-router": "^4.0.16" } -} +} \ No newline at end of file From 757946d4eef7771a74a53e524a6523c131701aab Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 18:22:47 +0800 Subject: [PATCH 017/168] Upgrade axios to v1 --- package.json | 3 ++- webpack/webpack.common.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 640856a80..5e1fd99b3 100644 --- a/package.json +++ b/package.json @@ -48,12 +48,13 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.0.4", - "axios": "^0.27.2", + "axios": "^1.1.3", "clipboardy": "^3.0.0", "countup.js": "^2.2.0", "echarts": "^5.3.3", "element-plus": "2.2.6", "psl": "^1.8.0", + "stream-browserify": "^3.0.0", "vue": "^3.2.37", "vue-router": "^4.0.16" } diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 7d7dbd71a..eed655b63 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -79,7 +79,17 @@ const staticOptions: webpack.Configuration = { }, resolve: { extensions: ['.ts', ".js", '.css', '.scss', '.sass'], - alias: resolveAlias + alias: resolveAlias, + fallback: { + // fallbacks of axios's dependencies start + stream: require.resolve('stream-browserify'), + zlib: false, + https: false, + http: false, + url: false, + assert: false, + // fallbacks of axios's dependencies end + } } } From 77432d79c2f33abdd325e947a4fbe9c903309381 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 18:38:54 +0800 Subject: [PATCH 018/168] Upgrade dependencies --- package.json | 38 +++++++++++++++++++------------------- test/__mock__/storage.ts | 7 +++---- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 5e1fd99b3..3b6a4cbda 100644 --- a/package.json +++ b/package.json @@ -15,47 +15,47 @@ "author": "zhy", "license": "MIT", "devDependencies": { - "@types/chrome": "0.0.190", + "@types/chrome": "0.0.199", "@types/copy-webpack-plugin": "^8.0.1", - "@types/echarts": "^4.9.15", + "@types/echarts": "^4.9.16", "@types/generate-json-webpack-plugin": "^0.3.4", "@types/jest": "^29.2.0", - "@types/node": "^18.0.0", + "@types/node": "^18.11.3", "@types/psl": "^1.1.0", "@types/webpack": "^5.28.0", - "@types/webpack-bundle-analyzer": "^4.4.1", + "@types/webpack-bundle-analyzer": "^4.6.0", "babel-loader": "^8.2.5", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.1", - "eslint": "^8.18.0", + "eslint": "^8.26.0", "filemanager-webpack-plugin": "^7.0.0", "generate-json-webpack-plugin": "^2.0.0", "jest": "^29.2.1", "jest-environment-jsdom": "^29.2.1", "mini-css-extract-plugin": "^2.6.1", - "node-sass": "^7.0.1", - "sass-loader": "^13.0.0", + "node-sass": "^7.0.3", + "sass-loader": "^13.1.0", "style-loader": "^3.3.1", "ts-jest": "^29.0.3", - "ts-loader": "^9.3.0", - "ts-node": "^10.8.1", + "ts-loader": "^9.4.1", + "ts-node": "^10.9.1", "tslib": "^2.4.0", - "typescript": "4.7.4", + "typescript": "4.8.4", "url-loader": "^4.1.1", - "webpack": "^5.73.0", - "webpack-bundle-analyzer": "^4.5.0", + "webpack": "^5.74.0", + "webpack-bundle-analyzer": "^4.6.1", "webpack-cli": "^4.10.0" }, "dependencies": { - "@element-plus/icons-vue": "^2.0.4", + "@element-plus/icons-vue": "^2.0.10", "axios": "^1.1.3", "clipboardy": "^3.0.0", - "countup.js": "^2.2.0", - "echarts": "^5.3.3", - "element-plus": "2.2.6", - "psl": "^1.8.0", + "countup.js": "^2.3.2", + "echarts": "^5.4.0", + "element-plus": "2.2.19", + "psl": "^1.9.0", "stream-browserify": "^3.0.0", - "vue": "^3.2.37", - "vue-router": "^4.0.16" + "vue": "^3.2.41", + "vue-router": "^4.1.5" } } \ No newline at end of file diff --git a/test/__mock__/storage.ts b/test/__mock__/storage.ts index 87300938f..f6e102d64 100644 --- a/test/__mock__/storage.ts +++ b/test/__mock__/storage.ts @@ -15,10 +15,9 @@ function resolveKey(key: string | Object | string[] | null) { resolveOneKey(key, result) return result } else if (Array.isArray(key)) { - return key.reduce((acc, curr) => { - resolveOneKey(curr, acc) - return acc - }, {}) + const result = {} + key.forEach(curr => resolveOneKey(curr, result)) + return result } else if (typeof key === 'object') { return Object.keys(key).reduce((acc, curr) => { acc[curr] = store[curr] || key[curr] From 22d0e47f617001609da76e834da173d3688481c5 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 23 Oct 2022 20:08:06 +0800 Subject: [PATCH 019/168] Update --- script/data/user_chrome.json | 19 ++++++++++++++++++- script/data/user_edge.json | 4 +++- script/data/user_firefox.json | 19 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/script/data/user_chrome.json b/script/data/user_chrome.json index 3d2fd215a..95f10853e 100644 --- a/script/data/user_chrome.json +++ b/script/data/user_chrome.json @@ -546,6 +546,13 @@ "2022-09-03": 1672, "2022-09-04": 1658, "2022-09-05": 1679, + "2022-09-06": 1695, + "2022-09-07": 1685, + "2022-09-08": 1683, + "2022-09-09": 1696, + "2022-09-10": 1696, + "2022-09-11": 1691, + "2022-09-12": 1684, "2022-09-13": 1702, "2022-09-14": 1716, "2022-09-15": 1725, @@ -574,5 +581,15 @@ "2022-10-08": 1755, "2022-10-09": 1791, "2022-10-10": 1802, - "2022-10-11": 1822 + "2022-10-11": 1822, + "2022-10-12": 1818, + "2022-10-13": 1801, + "2022-10-14": 1803, + "2022-10-15": 1828, + "2022-10-16": 1817, + "2022-10-17": 1820, + "2022-10-18": 1811, + "2022-10-19": 1803, + "2022-10-20": 1812, + "2022-10-21": 1833 } \ No newline at end of file diff --git a/script/data/user_edge.json b/script/data/user_edge.json index 8455f44f2..d899684ac 100644 --- a/script/data/user_edge.json +++ b/script/data/user_edge.json @@ -77,8 +77,10 @@ "2022-08-21": 1649, "2022-08-28": 1641, "2022-09-05": 1821, + "2022-09-11": 1719, "2022-09-18": 1851, "2022-09-25": 1805, "2022-10-02": 1827, - "2022-10-09": 1925 + "2022-10-09": 1925, + "2022-10-16": 1886 } \ No newline at end of file diff --git a/script/data/user_firefox.json b/script/data/user_firefox.json index d87a88972..95b96474f 100644 --- a/script/data/user_firefox.json +++ b/script/data/user_firefox.json @@ -548,6 +548,13 @@ "2022-09-04": 54, "2022-09-05": 66, "2022-09-06": 70, + "2022-09-07": 75, + "2022-09-08": 70, + "2022-09-09": 67, + "2022-09-10": 55, + "2022-09-11": 62, + "2022-09-12": 57, + "2022-09-13": 75, "2022-09-14": 67, "2022-09-15": 68, "2022-09-16": 71, @@ -576,5 +583,15 @@ "2022-10-09": 67, "2022-10-10": 62, "2022-10-11": 64, - "2022-10-12": 62 + "2022-10-12": 62, + "2022-10-13": 65, + "2022-10-14": 77, + "2022-10-15": 62, + "2022-10-16": 68, + "2022-10-17": 68, + "2022-10-18": 78, + "2022-10-19": 72, + "2022-10-20": 76, + "2022-10-21": 71, + "2022-10-22": 58 } \ No newline at end of file From d81e5d4bfff07506c088c1577ba4a6bed93aad8e Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 25 Oct 2022 21:04:31 +0800 Subject: [PATCH 020/168] Clean code (#155) --- public/popup.html | 9 --------- src/app/locale/components/menu.ts | 5 ----- 2 files changed, 14 deletions(-) diff --git a/public/popup.html b/public/popup.html index 8f929eff0..3dad01e6b 100644 --- a/public/popup.html +++ b/public/popup.html @@ -61,15 +61,6 @@ - +Timer (relaunch) - Timer is one browser extension to stat site visits and time. | Product Hunt From 74fdfd5a921ee390b90297b617b044d6005ce094 Mon Sep 17 00:00:00 2001 From: ZHY Date: Fri, 9 Dec 2022 05:51:15 +0000 Subject: [PATCH 056/168] Use nodejs to render chart --- package.json | 1 + script/data/user_chrome.json | 630 --------------------------------- script/data/user_edge.json | 92 ----- script/data/user_firefox.json | 632 ---------------------------------- script/requirements.txt | 3 - script/user-chart/add.ts | 140 ++++++++ script/user-chart/argv.ts | 70 ++++ script/user-chart/common.ts | 25 ++ script/user-chart/index.ts | 20 ++ script/user-chart/render.ts | 212 ++++++++++++ script/user_count.py | 316 ----------------- tsconfig.json | 8 +- 12 files changed, 475 insertions(+), 1674 deletions(-) delete mode 100644 script/data/user_chrome.json delete mode 100644 script/data/user_edge.json delete mode 100644 script/data/user_firefox.json delete mode 100644 script/requirements.txt create mode 100644 script/user-chart/add.ts create mode 100644 script/user-chart/argv.ts create mode 100644 script/user-chart/common.ts create mode 100644 script/user-chart/index.ts create mode 100644 script/user-chart/render.ts delete mode 100644 script/user_count.py diff --git a/package.json b/package.json index b67d38c7c..5c5a779be 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "ts-jest": "^29.0.3", "ts-loader": "^9.4.1", "ts-node": "^10.9.1", + "tsconfig-paths": "^4.1.1", "tslib": "^2.4.0", "typescript": "4.8.4", "url-loader": "^4.1.1", diff --git a/script/data/user_chrome.json b/script/data/user_chrome.json deleted file mode 100644 index c3a4559f1..000000000 --- a/script/data/user_chrome.json +++ /dev/null @@ -1,630 +0,0 @@ -{ - "2021-03-08": 4, - "2021-03-09": 5, - "2021-03-10": 6, - "2021-03-11": 6, - "2021-03-12": 9, - "2021-03-13": 10, - "2021-03-14": 11, - "2021-03-15": 12, - "2021-03-16": 13, - "2021-03-17": 15, - "2021-03-18": 16, - "2021-03-19": 16, - "2021-03-20": 17, - "2021-03-21": 17, - "2021-03-22": 15, - "2021-03-23": 15, - "2021-03-24": 15, - "2021-03-25": 15, - "2021-03-26": 13, - "2021-03-27": 15, - "2021-03-28": 15, - "2021-03-29": 16, - "2021-03-30": 16, - "2021-03-31": 15, - "2021-04-01": 15, - "2021-04-02": 15, - "2021-04-03": 15, - "2021-04-04": 15, - "2021-04-05": 16, - "2021-04-06": 17, - "2021-04-07": 17, - "2021-04-08": 18, - "2021-04-09": 19, - "2021-04-10": 19, - "2021-04-11": 19, - "2021-04-12": 21, - "2021-04-13": 21, - "2021-04-14": 23, - "2021-04-15": 23, - "2021-04-16": 23, - "2021-04-17": 22, - "2021-04-18": 23, - "2021-04-19": 22, - "2021-04-20": 22, - "2021-04-21": 22, - "2021-04-22": 23, - "2021-04-23": 25, - "2021-04-24": 23, - "2021-04-25": 23, - "2021-04-26": 23, - "2021-04-27": 25, - "2021-04-28": 27, - "2021-04-29": 29, - "2021-04-30": 27, - "2021-05-01": 27, - "2021-05-02": 28, - "2021-05-03": 29, - "2021-05-04": 29, - "2021-05-05": 27, - "2021-05-06": 26, - "2021-05-07": 27, - "2021-05-08": 28, - "2021-05-09": 28, - "2021-05-10": 29, - "2021-05-11": 29, - "2021-05-12": 30, - "2021-05-13": 30, - "2021-05-14": 33, - "2021-05-15": 35, - "2021-05-16": 36, - "2021-05-17": 36, - "2021-05-18": 36, - "2021-05-19": 35, - "2021-05-20": 36, - "2021-05-21": 34, - "2021-05-22": 33, - "2021-05-23": 33, - "2021-05-24": 34, - "2021-05-25": 35, - "2021-05-26": 35, - "2021-05-27": 38, - "2021-05-28": 41, - "2021-05-29": 41, - "2021-05-30": 43, - "2021-05-31": 52, - "2021-06-01": 58, - "2021-06-02": 62, - "2021-06-03": 65, - "2021-06-04": 65, - "2021-06-05": 66, - "2021-06-06": 66, - "2021-06-07": 70, - "2021-06-08": 70, - "2021-06-09": 71, - "2021-06-10": 73, - "2021-06-11": 74, - "2021-06-12": 75, - "2021-06-13": 75, - "2021-06-14": 75, - "2021-06-15": 73, - "2021-06-16": 74, - "2021-06-17": 75, - "2021-06-18": 77, - "2021-06-19": 78, - "2021-06-20": 78, - "2021-06-21": 78, - "2021-06-22": 79, - "2021-06-23": 81, - "2021-06-24": 81, - "2021-06-25": 83, - "2021-06-26": 84, - "2021-06-27": 85, - "2021-06-28": 89, - "2021-06-29": 88, - "2021-06-30": 88, - "2021-07-01": 89, - "2021-07-02": 87, - "2021-07-03": 90, - "2021-07-04": 91, - "2021-07-05": 92, - "2021-07-06": 102, - "2021-07-07": 105, - "2021-07-08": 109, - "2021-07-09": 116, - "2021-07-10": 117, - "2021-07-11": 121, - "2021-07-12": 122, - "2021-07-13": 128, - "2021-07-14": 129, - "2021-07-15": 131, - "2021-07-16": 136, - "2021-07-17": 138, - "2021-07-18": 136, - "2021-07-19": 138, - "2021-07-20": 135, - "2021-07-21": 141, - "2021-07-22": 142, - "2021-07-23": 144, - "2021-07-24": 148, - "2021-07-25": 153, - "2021-07-26": 159, - "2021-07-27": 162, - "2021-07-28": 158, - "2021-07-29": 160, - "2021-07-30": 162, - "2021-07-31": 163, - "2021-08-01": 163, - "2021-08-02": 165, - "2021-08-03": 168, - "2021-08-04": 172, - "2021-08-05": 174, - "2021-08-06": 174, - "2021-08-07": 174, - "2021-08-08": 174, - "2021-08-09": 174, - "2021-08-10": 172, - "2021-08-11": 171, - "2021-08-12": 168, - "2021-08-13": 167, - "2021-08-14": 170, - "2021-08-15": 178, - "2021-08-16": 180, - "2021-08-17": 184, - "2021-08-18": 187, - "2021-08-19": 186, - "2021-08-20": 190, - "2021-08-21": 191, - "2021-08-22": 193, - "2021-08-23": 195, - "2021-08-24": 192, - "2021-08-25": 189, - "2021-08-26": 191, - "2021-08-27": 187, - "2021-08-28": 189, - "2021-08-29": 192, - "2021-08-30": 190, - "2021-08-31": 171, - "2021-09-01": 169, - "2021-09-02": 165, - "2021-09-03": 168, - "2021-09-04": 167, - "2021-09-05": 169, - "2021-09-06": 171, - "2021-09-07": 169, - "2021-09-08": 172, - "2021-09-09": 172, - "2021-09-10": 167, - "2021-09-11": 166, - "2021-09-12": 164, - "2021-09-13": 157, - "2021-09-14": 157, - "2021-09-15": 156, - "2021-09-16": 159, - "2021-09-17": 158, - "2021-09-18": 161, - "2021-09-19": 163, - "2021-09-20": 167, - "2021-09-21": 168, - "2021-09-22": 167, - "2021-09-23": 170, - "2021-09-24": 171, - "2021-09-25": 170, - "2021-09-26": 171, - "2021-09-27": 172, - "2021-09-28": 171, - "2021-09-29": 174, - "2021-09-30": 176, - "2021-10-01": 180, - "2021-10-02": 183, - "2021-10-03": 184, - "2021-10-04": 184, - "2021-10-05": 181, - "2021-10-06": 168, - "2021-10-07": 178, - "2021-10-08": 183, - "2021-10-09": 186, - "2021-10-10": 190, - "2021-10-11": 194, - "2021-10-12": 197, - "2021-10-13": 197, - "2021-10-14": 199, - "2021-10-15": 205, - "2021-10-16": 209, - "2021-10-17": 219, - "2021-10-18": 218, - "2021-10-19": 222, - "2021-10-20": 217, - "2021-10-21": 217, - "2021-10-22": 209, - "2021-10-23": 204, - "2021-10-24": 204, - "2021-10-25": 203, - "2021-10-26": 202, - "2021-10-27": 198, - "2021-10-28": 197, - "2021-10-29": 197, - "2021-10-30": 195, - "2021-10-31": 199, - "2021-11-01": 196, - "2021-11-02": 194, - "2021-11-03": 197, - "2021-11-04": 193, - "2021-11-05": 192, - "2021-11-06": 193, - "2021-11-07": 192, - "2021-11-08": 192, - "2021-11-09": 195, - "2021-11-10": 197, - "2021-11-11": 200, - "2021-11-12": 193, - "2021-11-13": 189, - "2021-11-14": 187, - "2021-11-15": 193, - "2021-11-16": 195, - "2021-11-17": 194, - "2021-11-18": 196, - "2021-11-19": 204, - "2021-11-20": 207, - "2021-11-21": 212, - "2021-11-22": 214, - "2021-11-23": 215, - "2021-11-24": 214, - "2021-11-25": 215, - "2021-11-26": 211, - "2021-11-27": 209, - "2021-11-28": 209, - "2021-11-29": 211, - "2021-11-30": 209, - "2021-12-01": 213, - "2021-12-02": 216, - "2021-12-03": 219, - "2021-12-04": 219, - "2021-12-05": 220, - "2021-12-06": 223, - "2021-12-07": 222, - "2021-12-08": 218, - "2021-12-09": 218, - "2021-12-10": 222, - "2021-12-11": 225, - "2021-12-12": 222, - "2021-12-13": 219, - "2021-12-14": 217, - "2021-12-15": 219, - "2021-12-16": 220, - "2021-12-17": 219, - "2021-12-18": 221, - "2021-12-19": 225, - "2021-12-20": 233, - "2021-12-21": 234, - "2021-12-22": 236, - "2021-12-23": 242, - "2021-12-24": 239, - "2021-12-25": 239, - "2021-12-26": 240, - "2021-12-27": 234, - "2021-12-28": 229, - "2021-12-29": 228, - "2021-12-30": 228, - "2021-12-31": 235, - "2022-01-01": 240, - "2022-01-02": 242, - "2022-01-03": 240, - "2022-01-04": 242, - "2022-01-05": 245, - "2022-01-06": 246, - "2022-01-07": 242, - "2022-01-08": 241, - "2022-01-09": 240, - "2022-01-10": 240, - "2022-01-11": 240, - "2022-01-12": 238, - "2022-01-13": 242, - "2022-01-14": 238, - "2022-01-15": 241, - "2022-01-16": 239, - "2022-01-17": 244, - "2022-01-18": 245, - "2022-01-19": 247, - "2022-01-20": 247, - "2022-01-21": 249, - "2022-01-22": 244, - "2022-01-23": 247, - "2022-01-24": 251, - "2022-01-25": 259, - "2022-01-26": 262, - "2022-01-27": 267, - "2022-01-28": 266, - "2022-01-29": 272, - "2022-01-30": 302, - "2022-01-31": 304, - "2022-02-01": 288, - "2022-02-02": 282, - "2022-02-03": 269, - "2022-02-04": 258, - "2022-02-05": 257, - "2022-02-06": 290, - "2022-02-07": 315, - "2022-02-08": 334, - "2022-02-09": 337, - "2022-02-10": 344, - "2022-02-11": 352, - "2022-02-12": 355, - "2022-02-13": 363, - "2022-02-14": 366, - "2022-02-15": 369, - "2022-02-16": 379, - "2022-02-17": 376, - "2022-02-18": 387, - "2022-02-19": 385, - "2022-02-20": 387, - "2022-02-21": 395, - "2022-02-22": 400, - "2022-02-23": 398, - "2022-02-24": 402, - "2022-02-25": 405, - "2022-02-26": 403, - "2022-02-27": 406, - "2022-02-28": 413, - "2022-03-01": 412, - "2022-03-02": 422, - "2022-03-03": 419, - "2022-03-04": 423, - "2022-03-05": 424, - "2022-03-06": 426, - "2022-03-07": 421, - "2022-03-08": 419, - "2022-03-09": 420, - "2022-03-10": 433, - "2022-03-11": 437, - "2022-03-12": 442, - "2022-03-13": 441, - "2022-03-14": 440, - "2022-03-15": 442, - "2022-03-16": 441, - "2022-03-17": 432, - "2022-03-18": 430, - "2022-03-19": 435, - "2022-03-20": 438, - "2022-03-21": 453, - "2022-03-22": 463, - "2022-03-23": 481, - "2022-03-24": 491, - "2022-03-25": 485, - "2022-03-26": 482, - "2022-03-27": 477, - "2022-03-28": 467, - "2022-03-29": 463, - "2022-03-30": 465, - "2022-03-31": 470, - "2022-04-01": 468, - "2022-04-02": 463, - "2022-04-03": 465, - "2022-04-04": 462, - "2022-04-05": 462, - "2022-04-06": 465, - "2022-04-07": 466, - "2022-04-08": 467, - "2022-04-09": 460, - "2022-04-10": 463, - "2022-04-11": 472, - "2022-04-12": 474, - "2022-04-13": 468, - "2022-04-14": 478, - "2022-04-15": 486, - "2022-04-16": 492, - "2022-04-17": 495, - "2022-04-18": 499, - "2022-04-19": 500, - "2022-04-20": 505, - "2022-04-21": 624, - "2022-04-22": 689, - "2022-04-23": 735, - "2022-04-24": 773, - "2022-04-25": 797, - "2022-04-26": 822, - "2022-04-27": 848, - "2022-04-28": 936, - "2022-04-29": 1019, - "2022-04-30": 1058, - "2022-05-01": 1089, - "2022-05-02": 1123, - "2022-05-03": 1130, - "2022-05-04": 1132, - "2022-05-05": 1148, - "2022-05-06": 1171, - "2022-05-07": 1180, - "2022-05-08": 1206, - "2022-05-09": 1217, - "2022-05-10": 1223, - "2022-05-11": 1230, - "2022-05-12": 1238, - "2022-05-13": 1247, - "2022-05-14": 1245, - "2022-05-15": 1249, - "2022-05-16": 1248, - "2022-05-17": 1255, - "2022-05-18": 1262, - "2022-05-19": 1256, - "2022-05-20": 1258, - "2022-05-21": 1262, - "2022-05-22": 1257, - "2022-05-23": 1274, - "2022-05-24": 1285, - "2022-05-25": 1294, - "2022-05-26": 1301, - "2022-05-27": 1312, - "2022-05-28": 1310, - "2022-05-29": 1315, - "2022-05-30": 1309, - "2022-05-31": 1313, - "2022-06-01": 1313, - "2022-06-02": 1315, - "2022-06-03": 1321, - "2022-06-04": 1328, - "2022-06-05": 1330, - "2022-06-06": 1329, - "2022-06-07": 1331, - "2022-06-08": 1331, - "2022-06-09": 1326, - "2022-06-10": 1323, - "2022-06-11": 1315, - "2022-06-12": 1323, - "2022-06-13": 1337, - "2022-06-14": 1349, - "2022-06-15": 1354, - "2022-06-16": 1361, - "2022-06-17": 1369, - "2022-06-18": 1359, - "2022-06-19": 1383, - "2022-06-20": 1402, - "2022-06-21": 1399, - "2022-06-22": 1399, - "2022-06-23": 1422, - "2022-06-24": 1451, - "2022-06-25": 1474, - "2022-06-26": 1464, - "2022-06-27": 1481, - "2022-06-28": 1479, - "2022-06-29": 1465, - "2022-06-30": 1423, - "2022-07-01": 1414, - "2022-07-02": 1392, - "2022-07-03": 1440, - "2022-07-04": 1466, - "2022-07-05": 1473, - "2022-07-06": 1476, - "2022-07-07": 1484, - "2022-07-08": 1481, - "2022-07-09": 1479, - "2022-07-10": 1501, - "2022-07-11": 1514, - "2022-07-12": 1529, - "2022-07-13": 1532, - "2022-07-14": 1547, - "2022-07-15": 1567, - "2022-07-16": 1576, - "2022-07-17": 1583, - "2022-07-18": 1585, - "2022-07-19": 1607, - "2022-07-20": 1601, - "2022-07-21": 1613, - "2022-07-22": 1602, - "2022-07-23": 1602, - "2022-07-24": 1623, - "2022-07-25": 1623, - "2022-07-26": 1617, - "2022-07-27": 1624, - "2022-07-28": 1612, - "2022-07-29": 1620, - "2022-07-30": 1624, - "2022-07-31": 1612, - "2022-08-01": 1618, - "2022-08-02": 1636, - "2022-08-03": 1632, - "2022-08-04": 1628, - "2022-08-05": 1631, - "2022-08-06": 1629, - "2022-08-07": 1628, - "2022-08-08": 1645, - "2022-08-09": 1652, - "2022-08-10": 1664, - "2022-08-11": 1654, - "2022-08-12": 1643, - "2022-08-13": 1634, - "2022-08-14": 1646, - "2022-08-15": 1652, - "2022-08-16": 1646, - "2022-08-17": 1639, - "2022-08-18": 1632, - "2022-08-19": 1636, - "2022-08-20": 1634, - "2022-08-21": 1638, - "2022-08-22": 1650, - "2022-08-23": 1648, - "2022-08-24": 1662, - "2022-08-25": 1664, - "2022-08-26": 1665, - "2022-08-27": 1669, - "2022-08-28": 1679, - "2022-08-29": 1671, - "2022-08-30": 1668, - "2022-08-31": 1675, - "2022-09-01": 1663, - "2022-09-02": 1659, - "2022-09-03": 1672, - "2022-09-04": 1658, - "2022-09-05": 1679, - "2022-09-06": 1695, - "2022-09-07": 1685, - "2022-09-08": 1683, - "2022-09-09": 1696, - "2022-09-10": 1696, - "2022-09-11": 1691, - "2022-09-12": 1684, - "2022-09-13": 1702, - "2022-09-14": 1716, - "2022-09-15": 1725, - "2022-09-16": 1718, - "2022-09-17": 1716, - "2022-09-18": 1725, - "2022-09-19": 1747, - "2022-09-20": 1749, - "2022-09-21": 1739, - "2022-09-22": 1742, - "2022-09-23": 1754, - "2022-09-24": 1745, - "2022-09-25": 1752, - "2022-09-26": 1753, - "2022-09-27": 1770, - "2022-09-28": 1778, - "2022-09-29": 1794, - "2022-09-30": 1793, - "2022-10-01": 1791, - "2022-10-02": 1789, - "2022-10-03": 1778, - "2022-10-04": 1756, - "2022-10-05": 1726, - "2022-10-06": 1634, - "2022-10-07": 1705, - "2022-10-08": 1755, - "2022-10-09": 1791, - "2022-10-10": 1802, - "2022-10-11": 1822, - "2022-10-12": 1818, - "2022-10-13": 1801, - "2022-10-14": 1803, - "2022-10-15": 1828, - "2022-10-16": 1817, - "2022-10-17": 1820, - "2022-10-18": 1811, - "2022-10-19": 1803, - "2022-10-20": 1812, - "2022-10-21": 1833, - "2022-10-22": 1831, - "2022-10-23": 1838, - "2022-10-24": 1830, - "2022-10-25": 1830, - "2022-10-26": 1823, - "2022-10-27": 1832, - "2022-10-28": 1822, - "2022-10-29": 1820, - "2022-10-30": 1831, - "2022-10-31": 1824, - "2022-11-01": 1816, - "2022-11-02": 1824, - "2022-11-03": 1838, - "2022-11-04": 1841, - "2022-11-05": 1854, - "2022-11-06": 1844, - "2022-11-07": 1860, - "2022-11-08": 1855, - "2022-11-09": 1857, - "2022-11-10": 1858, - "2022-11-11": 1850, - "2022-11-12": 1854, - "2022-11-13": 1853, - "2022-11-14": 1848, - "2022-11-15": 1862, - "2022-11-16": 1867, - "2022-11-17": 1877, - "2022-11-18": 1894, - "2022-11-19": 1879, - "2022-11-20": 1874, - "2022-11-21": 1882, - "2022-11-22": 1888, - "2022-11-23": 1883, - "2022-11-24": 1887, - "2022-11-25": 1904 -} \ No newline at end of file diff --git a/script/data/user_edge.json b/script/data/user_edge.json deleted file mode 100644 index 8f5f2a2fd..000000000 --- a/script/data/user_edge.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "2021-03-14": 19, - "2021-03-21": 49, - "2021-03-28": 57, - "2021-04-04": 63, - "2021-04-11": 63, - "2021-04-18": 74, - "2021-04-25": 93, - "2021-05-02": 118, - "2021-05-09": 131, - "2021-05-16": 133, - "2021-05-23": 154, - "2021-05-30": 169, - "2021-06-06": 193, - "2021-06-13": 192, - "2021-06-20": 203, - "2021-06-27": 209, - "2021-07-04": 258, - "2021-07-11": 219, - "2021-07-18": 227, - "2021-07-25": 246, - "2021-08-01": 271, - "2021-08-08": 291, - "2021-08-15": 298, - "2021-08-22": 295, - "2021-08-29": 313, - "2021-09-05": 305, - "2021-09-12": 311, - "2021-09-19": 356, - "2021-09-26": 324, - "2021-10-03": 346, - "2021-10-10": 417, - "2021-10-17": 399, - "2021-10-24": 378, - "2021-10-31": 394, - "2021-11-07": 460, - "2021-11-14": 449, - "2021-11-21": 438, - "2021-11-28": 432, - "2021-12-05": 453, - "2021-12-12": 461, - "2021-12-19": 465, - "2021-12-26": 509, - "2022-01-02": 524, - "2022-01-09": 530, - "2022-01-16": 519, - "2022-01-23": 517, - "2022-01-30": 528, - "2022-02-06": 459, - "2022-02-13": 582, - "2022-02-20": 546, - "2022-02-27": 591, - "2022-03-06": 618, - "2022-03-13": 645, - "2022-03-20": 646, - "2022-03-27": 641, - "2022-04-03": 692, - "2022-04-10": 744, - "2022-04-17": 747, - "2022-04-24": 975, - "2022-05-01": 1198, - "2022-05-08": 1294, - "2022-05-15": 1302, - "2022-05-22": 1354, - "2022-05-29": 1369, - "2022-06-05": 1354, - "2022-06-12": 1360, - "2022-06-19": 1398, - "2022-06-26": 1378, - "2022-07-03": 1379, - "2022-07-10": 1553, - "2022-07-17": 1532, - "2022-07-24": 1701, - "2022-07-31": 1728, - "2022-08-07": 1644, - "2022-08-14": 1636, - "2022-08-21": 1649, - "2022-08-28": 1641, - "2022-09-05": 1821, - "2022-09-11": 1719, - "2022-09-18": 1851, - "2022-09-25": 1805, - "2022-10-02": 1827, - "2022-10-09": 1925, - "2022-10-16": 1886, - "2022-10-23": 1906, - "2022-10-30": 1888, - "2022-11-06": 1905, - "2022-11-13": 1967, - "2022-11-20": 1956, - "2022-11-27": 1993 -} \ No newline at end of file diff --git a/script/data/user_firefox.json b/script/data/user_firefox.json deleted file mode 100644 index b52c4cff2..000000000 --- a/script/data/user_firefox.json +++ /dev/null @@ -1,632 +0,0 @@ -{ - "2021-03-03": 1, - "2021-03-04": 2, - "2021-03-05": 1, - "2021-03-06": 1, - "2021-03-08": 1, - "2021-03-09": 2, - "2021-03-10": 1, - "2021-03-11": 1, - "2021-03-12": 1, - "2021-03-13": 1, - "2021-03-14": 1, - "2021-03-15": 1, - "2021-03-16": 1, - "2021-03-17": 1, - "2021-03-18": 1, - "2021-03-19": 1, - "2021-03-22": 1, - "2021-03-23": 2, - "2021-03-24": 3, - "2021-03-25": 1, - "2021-03-26": 2, - "2021-03-27": 2, - "2021-03-28": 2, - "2021-03-30": 2, - "2021-03-31": 1, - "2021-04-01": 2, - "2021-04-02": 2, - "2021-04-03": 1, - "2021-04-04": 1, - "2021-04-05": 1, - "2021-04-06": 1, - "2021-04-07": 2, - "2021-04-08": 1, - "2021-04-09": 1, - "2021-04-10": 2, - "2021-04-11": 3, - "2021-04-12": 3, - "2021-04-13": 3, - "2021-04-14": 4, - "2021-04-15": 4, - "2021-04-16": 6, - "2021-04-17": 5, - "2021-04-18": 4, - "2021-04-19": 4, - "2021-04-20": 3, - "2021-04-21": 5, - "2021-04-22": 3, - "2021-04-23": 3, - "2021-04-24": 3, - "2021-04-25": 4, - "2021-04-26": 5, - "2021-04-27": 4, - "2021-04-28": 6, - "2021-04-29": 4, - "2021-04-30": 6, - "2021-05-01": 4, - "2021-05-02": 5, - "2021-05-03": 6, - "2021-05-04": 5, - "2021-05-05": 6, - "2021-05-06": 7, - "2021-05-07": 7, - "2021-05-08": 6, - "2021-05-09": 6, - "2021-05-10": 7, - "2021-05-11": 10, - "2021-05-12": 8, - "2021-05-13": 9, - "2021-05-14": 9, - "2021-05-15": 6, - "2021-05-16": 7, - "2021-05-17": 7, - "2021-05-18": 8, - "2021-05-19": 7, - "2021-05-20": 8, - "2021-05-21": 9, - "2021-05-22": 8, - "2021-05-23": 8, - "2021-05-24": 7, - "2021-05-25": 7, - "2021-05-26": 8, - "2021-05-27": 8, - "2021-05-28": 9, - "2021-05-29": 9, - "2021-05-30": 7, - "2021-05-31": 10, - "2021-06-01": 9, - "2021-06-02": 10, - "2021-06-03": 8, - "2021-06-04": 9, - "2021-06-05": 9, - "2021-06-06": 9, - "2021-06-07": 10, - "2021-06-08": 11, - "2021-06-09": 12, - "2021-06-10": 11, - "2021-06-11": 10, - "2021-06-12": 10, - "2021-06-13": 12, - "2021-06-14": 9, - "2021-06-15": 10, - "2021-06-16": 8, - "2021-06-17": 10, - "2021-06-18": 10, - "2021-06-19": 10, - "2021-06-20": 9, - "2021-06-21": 7, - "2021-06-22": 9, - "2021-06-23": 9, - "2021-06-24": 11, - "2021-06-25": 12, - "2021-06-26": 12, - "2021-06-27": 11, - "2021-06-28": 12, - "2021-06-29": 11, - "2021-06-30": 11, - "2021-07-01": 11, - "2021-07-02": 8, - "2021-07-03": 11, - "2021-07-04": 9, - "2021-07-05": 14, - "2021-07-06": 12, - "2021-07-07": 12, - "2021-07-08": 12, - "2021-07-09": 12, - "2021-07-10": 13, - "2021-07-11": 14, - "2021-07-12": 13, - "2021-07-13": 11, - "2021-07-14": 14, - "2021-07-15": 14, - "2021-07-16": 16, - "2021-07-17": 12, - "2021-07-18": 9, - "2021-07-19": 12, - "2021-07-20": 10, - "2021-07-21": 8, - "2021-07-22": 11, - "2021-07-23": 12, - "2021-07-24": 10, - "2021-07-25": 10, - "2021-07-26": 10, - "2021-07-27": 12, - "2021-07-28": 10, - "2021-07-29": 9, - "2021-07-30": 9, - "2021-07-31": 10, - "2021-08-01": 11, - "2021-08-02": 11, - "2021-08-03": 14, - "2021-08-04": 15, - "2021-08-05": 14, - "2021-08-06": 17, - "2021-08-07": 17, - "2021-08-08": 14, - "2021-08-09": 14, - "2021-08-10": 15, - "2021-08-11": 14, - "2021-08-12": 14, - "2021-08-13": 11, - "2021-08-14": 12, - "2021-08-15": 13, - "2021-08-16": 12, - "2021-08-17": 9, - "2021-08-18": 13, - "2021-08-19": 11, - "2021-08-20": 9, - "2021-08-21": 9, - "2021-08-22": 10, - "2021-08-23": 11, - "2021-08-24": 13, - "2021-08-25": 12, - "2021-08-26": 13, - "2021-08-27": 12, - "2021-08-28": 12, - "2021-08-29": 18, - "2021-08-30": 20, - "2021-08-31": 16, - "2021-09-01": 17, - "2021-09-02": 17, - "2021-09-03": 17, - "2021-09-04": 18, - "2021-09-05": 18, - "2021-09-06": 18, - "2021-09-07": 17, - "2021-09-08": 16, - "2021-09-09": 16, - "2021-09-10": 15, - "2021-09-11": 15, - "2021-09-12": 15, - "2021-09-13": 14, - "2021-09-14": 19, - "2021-09-15": 19, - "2021-09-16": 17, - "2021-09-17": 16, - "2021-09-18": 15, - "2021-09-19": 15, - "2021-09-20": 14, - "2021-09-21": 13, - "2021-09-22": 17, - "2021-09-23": 16, - "2021-09-24": 17, - "2021-09-25": 18, - "2021-09-26": 14, - "2021-09-27": 15, - "2021-09-28": 12, - "2021-09-29": 10, - "2021-09-30": 3, - "2021-10-01": 3, - "2021-10-02": 5, - "2021-10-03": 3, - "2021-10-04": 2, - "2021-10-05": 4, - "2021-10-06": 5, - "2021-10-07": 11, - "2021-10-08": 12, - "2021-10-09": 12, - "2021-10-10": 17, - "2021-10-11": 15, - "2021-10-12": 14, - "2021-10-13": 13, - "2021-10-14": 12, - "2021-10-15": 13, - "2021-10-16": 13, - "2021-10-17": 14, - "2021-10-18": 11, - "2021-10-19": 14, - "2021-10-20": 12, - "2021-10-21": 12, - "2021-10-22": 13, - "2021-10-23": 15, - "2021-10-24": 16, - "2021-10-25": 17, - "2021-10-26": 13, - "2021-10-27": 13, - "2021-10-28": 11, - "2021-10-29": 16, - "2021-10-30": 18, - "2021-10-31": 17, - "2021-11-01": 15, - "2021-11-02": 11, - "2021-11-03": 14, - "2021-11-04": 16, - "2021-11-05": 16, - "2021-11-06": 17, - "2021-11-07": 14, - "2021-11-08": 14, - "2021-11-09": 15, - "2021-11-10": 17, - "2021-11-11": 14, - "2021-11-12": 18, - "2021-11-13": 12, - "2021-11-14": 15, - "2021-11-15": 15, - "2021-11-16": 16, - "2021-11-17": 17, - "2021-11-18": 12, - "2021-11-19": 12, - "2021-11-20": 12, - "2021-11-21": 17, - "2021-11-22": 16, - "2021-11-23": 18, - "2021-11-24": 18, - "2021-11-25": 17, - "2021-11-26": 16, - "2021-11-27": 18, - "2021-11-28": 16, - "2021-11-29": 16, - "2021-11-30": 11, - "2021-12-01": 12, - "2021-12-02": 13, - "2021-12-03": 16, - "2021-12-04": 13, - "2021-12-05": 13, - "2021-12-06": 12, - "2021-12-07": 13, - "2021-12-08": 14, - "2021-12-09": 12, - "2021-12-10": 10, - "2021-12-11": 12, - "2021-12-12": 15, - "2021-12-13": 14, - "2021-12-14": 15, - "2021-12-15": 15, - "2021-12-16": 14, - "2021-12-17": 14, - "2021-12-18": 11, - "2021-12-19": 12, - "2021-12-20": 18, - "2021-12-21": 15, - "2021-12-22": 16, - "2021-12-23": 15, - "2021-12-24": 12, - "2021-12-25": 9, - "2021-12-26": 11, - "2021-12-27": 11, - "2021-12-28": 12, - "2021-12-29": 11, - "2021-12-30": 11, - "2021-12-31": 14, - "2022-01-01": 11, - "2022-01-02": 15, - "2022-01-03": 12, - "2022-01-04": 15, - "2022-01-05": 13, - "2022-01-06": 14, - "2022-01-07": 10, - "2022-01-08": 9, - "2022-01-09": 7, - "2022-01-10": 11, - "2022-01-11": 12, - "2022-01-12": 12, - "2022-01-13": 14, - "2022-01-14": 11, - "2022-01-15": 13, - "2022-01-16": 13, - "2022-01-17": 18, - "2022-01-18": 17, - "2022-01-19": 16, - "2022-01-20": 19, - "2022-01-21": 14, - "2022-01-22": 12, - "2022-01-23": 15, - "2022-01-24": 20, - "2022-01-25": 16, - "2022-01-26": 21, - "2022-01-27": 18, - "2022-01-28": 15, - "2022-01-29": 13, - "2022-01-30": 11, - "2022-01-31": 10, - "2022-02-01": 12, - "2022-02-02": 10, - "2022-02-03": 11, - "2022-02-04": 13, - "2022-02-05": 16, - "2022-02-06": 15, - "2022-02-07": 12, - "2022-02-08": 16, - "2022-02-09": 16, - "2022-02-10": 19, - "2022-02-11": 19, - "2022-02-12": 20, - "2022-02-13": 15, - "2022-02-14": 15, - "2022-02-15": 17, - "2022-02-16": 19, - "2022-02-17": 14, - "2022-02-18": 21, - "2022-02-19": 15, - "2022-02-20": 16, - "2022-02-21": 18, - "2022-02-22": 20, - "2022-02-23": 17, - "2022-02-24": 17, - "2022-02-25": 16, - "2022-02-26": 15, - "2022-02-27": 18, - "2022-02-28": 18, - "2022-03-01": 20, - "2022-03-02": 18, - "2022-03-03": 18, - "2022-03-04": 18, - "2022-03-05": 21, - "2022-03-06": 17, - "2022-03-07": 19, - "2022-03-08": 19, - "2022-03-09": 20, - "2022-03-10": 20, - "2022-03-11": 19, - "2022-03-12": 20, - "2022-03-13": 21, - "2022-03-14": 20, - "2022-03-15": 22, - "2022-03-16": 22, - "2022-03-17": 24, - "2022-03-18": 21, - "2022-03-19": 21, - "2022-03-20": 18, - "2022-03-21": 20, - "2022-03-22": 17, - "2022-03-23": 17, - "2022-03-24": 16, - "2022-03-25": 19, - "2022-03-26": 21, - "2022-03-27": 21, - "2022-03-28": 21, - "2022-03-29": 18, - "2022-03-30": 20, - "2022-03-31": 21, - "2022-04-01": 18, - "2022-04-02": 17, - "2022-04-03": 18, - "2022-04-04": 18, - "2022-04-05": 20, - "2022-04-06": 18, - "2022-04-07": 19, - "2022-04-08": 23, - "2022-04-09": 18, - "2022-04-10": 15, - "2022-04-11": 18, - "2022-04-12": 18, - "2022-04-13": 22, - "2022-04-14": 21, - "2022-04-15": 22, - "2022-04-16": 19, - "2022-04-17": 25, - "2022-04-18": 18, - "2022-04-19": 17, - "2022-04-20": 20, - "2022-04-21": 34, - "2022-04-22": 42, - "2022-04-23": 43, - "2022-04-24": 59, - "2022-04-25": 67, - "2022-04-26": 66, - "2022-04-27": 59, - "2022-04-28": 62, - "2022-04-29": 53, - "2022-04-30": 46, - "2022-05-01": 50, - "2022-05-02": 45, - "2022-05-03": 54, - "2022-05-04": 61, - "2022-05-05": 65, - "2022-05-06": 57, - "2022-05-07": 59, - "2022-05-08": 52, - "2022-05-09": 56, - "2022-05-10": 62, - "2022-05-11": 60, - "2022-05-12": 57, - "2022-05-13": 59, - "2022-05-14": 49, - "2022-05-15": 54, - "2022-05-16": 58, - "2022-05-17": 59, - "2022-05-18": 55, - "2022-05-19": 64, - "2022-05-20": 63, - "2022-05-21": 60, - "2022-05-22": 61, - "2022-05-23": 63, - "2022-05-24": 60, - "2022-05-25": 56, - "2022-05-26": 61, - "2022-05-27": 56, - "2022-05-28": 58, - "2022-05-29": 53, - "2022-05-30": 56, - "2022-05-31": 51, - "2022-06-01": 59, - "2022-06-02": 65, - "2022-06-03": 53, - "2022-06-04": 54, - "2022-06-05": 51, - "2022-06-06": 64, - "2022-06-07": 61, - "2022-06-08": 56, - "2022-06-09": 59, - "2022-06-10": 63, - "2022-06-11": 47, - "2022-06-12": 55, - "2022-06-13": 67, - "2022-06-14": 57, - "2022-06-15": 63, - "2022-06-16": 60, - "2022-06-17": 56, - "2022-06-18": 54, - "2022-06-19": 54, - "2022-06-20": 61, - "2022-06-21": 60, - "2022-06-22": 68, - "2022-06-23": 63, - "2022-06-24": 63, - "2022-06-25": 53, - "2022-06-26": 48, - "2022-06-27": 59, - "2022-06-28": 61, - "2022-06-29": 57, - "2022-06-30": 58, - "2022-07-01": 56, - "2022-07-02": 46, - "2022-07-03": 50, - "2022-07-04": 60, - "2022-07-05": 57, - "2022-07-06": 61, - "2022-07-07": 68, - "2022-07-08": 62, - "2022-07-09": 52, - "2022-07-10": 48, - "2022-07-11": 55, - "2022-07-12": 62, - "2022-07-13": 57, - "2022-07-14": 55, - "2022-07-15": 61, - "2022-07-16": 52, - "2022-07-17": 49, - "2022-07-18": 58, - "2022-07-19": 56, - "2022-07-20": 61, - "2022-07-21": 59, - "2022-07-22": 61, - "2022-07-23": 60, - "2022-07-24": 51, - "2022-07-25": 66, - "2022-07-26": 70, - "2022-07-27": 70, - "2022-07-28": 65, - "2022-07-29": 67, - "2022-07-30": 56, - "2022-07-31": 55, - "2022-08-01": 68, - "2022-08-02": 71, - "2022-08-03": 58, - "2022-08-04": 57, - "2022-08-05": 61, - "2022-08-06": 63, - "2022-08-07": 65, - "2022-08-08": 68, - "2022-08-09": 60, - "2022-08-10": 73, - "2022-08-11": 63, - "2022-08-12": 60, - "2022-08-13": 52, - "2022-08-14": 49, - "2022-08-15": 64, - "2022-08-16": 63, - "2022-08-17": 67, - "2022-08-18": 67, - "2022-08-19": 73, - "2022-08-20": 61, - "2022-08-21": 61, - "2022-08-22": 71, - "2022-08-23": 69, - "2022-08-24": 67, - "2022-08-25": 66, - "2022-08-26": 64, - "2022-08-27": 57, - "2022-08-28": 51, - "2022-08-29": 68, - "2022-08-30": 65, - "2022-08-31": 69, - "2022-09-01": 60, - "2022-09-02": 67, - "2022-09-03": 61, - "2022-09-04": 54, - "2022-09-05": 66, - "2022-09-06": 70, - "2022-09-07": 75, - "2022-09-08": 70, - "2022-09-09": 67, - "2022-09-10": 55, - "2022-09-11": 62, - "2022-09-12": 57, - "2022-09-13": 75, - "2022-09-14": 67, - "2022-09-15": 68, - "2022-09-16": 71, - "2022-09-17": 61, - "2022-09-18": 52, - "2022-09-19": 62, - "2022-09-20": 58, - "2022-09-21": 59, - "2022-09-22": 62, - "2022-09-23": 67, - "2022-09-24": 63, - "2022-09-25": 54, - "2022-09-26": 65, - "2022-09-27": 70, - "2022-09-28": 65, - "2022-09-29": 55, - "2022-09-30": 60, - "2022-10-01": 49, - "2022-10-02": 52, - "2022-10-03": 54, - "2022-10-04": 56, - "2022-10-05": 66, - "2022-10-06": 61, - "2022-10-07": 60, - "2022-10-08": 67, - "2022-10-09": 67, - "2022-10-10": 62, - "2022-10-11": 64, - "2022-10-12": 62, - "2022-10-13": 65, - "2022-10-14": 77, - "2022-10-15": 62, - "2022-10-16": 68, - "2022-10-17": 68, - "2022-10-18": 78, - "2022-10-19": 72, - "2022-10-20": 76, - "2022-10-21": 71, - "2022-10-22": 58, - "2022-10-23": 59, - "2022-10-24": 68, - "2022-10-25": 70, - "2022-10-26": 69, - "2022-10-27": 74, - "2022-10-28": 65, - "2022-10-29": 57, - "2022-10-30": 63, - "2022-10-31": 75, - "2022-11-01": 77, - "2022-11-02": 77, - "2022-11-03": 78, - "2022-11-04": 79, - "2022-11-05": 66, - "2022-11-06": 63, - "2022-11-07": 69, - "2022-11-08": 67, - "2022-11-09": 64, - "2022-11-10": 73, - "2022-11-11": 73, - "2022-11-12": 61, - "2022-11-13": 59, - "2022-11-14": 68, - "2022-11-15": 74, - "2022-11-16": 73, - "2022-11-17": 77, - "2022-11-18": 79, - "2022-11-19": 65, - "2022-11-20": 64, - "2022-11-21": 73, - "2022-11-22": 76, - "2022-11-23": 78, - "2022-11-24": 74, - "2022-11-25": 78, - "2022-11-26": 66 -} \ No newline at end of file diff --git a/script/requirements.txt b/script/requirements.txt deleted file mode 100644 index f04644a00..000000000 --- a/script/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pygal -cairosvg -requests \ No newline at end of file diff --git a/script/user-chart/add.ts b/script/user-chart/add.ts new file mode 100644 index 000000000..2a1d54fdf --- /dev/null +++ b/script/user-chart/add.ts @@ -0,0 +1,140 @@ +import { + createGist as createGistApi, + getJsonFileContent, + Gist, + GistForm, + updateGist as updateGistApi +} from "@src/api/gist" +import { AddArgv, Argv, Browser } from "./argv" +import fs from "fs" +import { descriptionOf, exitWith, filenameOf, getExistGist } from "./common" + +export type UserCount = Record + +async function createGist(token: string, browser: Browser, data: UserCount) { + const description = descriptionOf(browser) + const filename = filenameOf(browser) + + // 1. sort by key + const sorted: UserCount = {} + Object.keys(data).sort().forEach(key => sorted[key] = data[key]) + // 2. create + const files = {} + files[filename] = { filename: filename, content: JSON.stringify(sorted, null, 2) } + const gistForm: GistForm = { + public: true, + description, + files + } + await createGistApi(token, gistForm) +} + +async function updateGist(token: string, browser: Browser, data: UserCount, gist: Gist) { + const description = descriptionOf(browser) + const filename = filenameOf(browser) + // 1. merge + const file = gist.files[filename] + const existData = (await getJsonFileContent(file)) || {} + Object.entries(data).forEach(([key, val]) => existData[key] = val) + // 2. sort by key + const sorted: UserCount = {} + Object.keys(existData).sort().forEach(key => sorted[key] = existData[key]) + const files = {} + files[filename] = { filename: filename, content: JSON.stringify(sorted, null, 2) } + const gistForm: GistForm = { + public: true, + description, + files + } + updateGistApi(token, gist.id, gistForm) +} + +function parseChrome(content: string): UserCount { + const lines = content.split('\n') + const result = {} + if (!(lines?.length > 2)) { + return result + } + lines.slice(2).forEach(line => { + const [dateStr, numberStr] = line.split(',') + if (!dateStr || !numberStr) { + return + } + // Replace '/' to '-', then rjust month and date + const date = dateStr.split('/').map(str => rjust(str, 2, '0')).join('-') + const number = parseInt(numberStr) + date && number && (result[date] = number) + }) + return result +} + +function parseEdge(content: string): UserCount { + const lines = content.split('\n') + const result = {} + if (!(lines?.length > 1)) { + return result + } + lines.slice(1).forEach(line => { + const splits = line.split(',') + const dateStr = splits[5] + const numberStr = splits[6] + if (!dateStr || !numberStr) { + return + } + // Replace '/' to '-', then rjust month and date + const date = dateStr.split('/').map(str => rjust(str, 2, '0')).join('-') + const number = parseInt(numberStr) + date && number && (result[date] = number) + }) + return result +} + +function parseFirefox(content: string): UserCount { + const lines = content.split('\n') + const result = {} + if (!(lines?.length > 4)) { + return result + } + lines.slice(4).forEach(line => { + const splits = line.split(',') + const date = splits[0] + const numberStr = splits[1] + if (!date || !numberStr) { + return + } + const number = parseInt(numberStr) + date && number && (result[date] = number) + }) + return result +} + +function rjust(str: string, num: number, padding: string): string { + str = str || '' + if (str.length >= num) { + return str + } + return Array.from(new Array(num - str.length).keys()).map(_ => padding).join('') + str +} + +export async function add(argv: Argv) { + const token = argv.gistToken + const browser = (argv as AddArgv).browser + const fileName = (argv as AddArgv).fileName + const content = fs.readFileSync(fileName, { encoding: 'utf-8' }) + let newData: UserCount = {} + if (browser === 'chrome') { + newData = parseChrome(content) + } else if (browser === 'edge') { + newData = parseEdge(content) + } else if (browser === 'firefox') { + newData = parseFirefox(content) + } else { + exitWith("Un-supported browser: " + browser) + } + const gist = await getExistGist(token, browser) + if (!gist) { + await createGist(token, browser, newData) + } else { + await updateGist(token, browser, newData, gist) + } +} \ No newline at end of file diff --git a/script/user-chart/argv.ts b/script/user-chart/argv.ts new file mode 100644 index 000000000..122c3033d --- /dev/null +++ b/script/user-chart/argv.ts @@ -0,0 +1,70 @@ +import { exitWith } from "./common" + +type Cmd = + | 'add' + | 'render' + +type ArgvBase = { + cmd: Cmd + gistToken: string +} + +export type RenderArgv = ArgvBase & { + cmd: 'render' +} + +export type Browser = + | 'chrome' + | 'firefox' + | 'edge' + +export type AddArgv = ArgvBase & { + cmd: 'add' + browser: Browser + fileName: string +} + +export type Argv = + | RenderArgv + | AddArgv + +export function parseArgv(): Argv { + const argv = process.argv.slice(2) + const cmd: Cmd = argv[0] as Cmd + + const token = process.env.TIMER_USER_COUNT_GIST_TOKEN + if (!token) { + exitWith("Can't find token from env variable [TIMER_USER_COUNT_GIST_TOKEN]") + } + if (cmd === 'add') { + return parseAddArgv(argv, token) + } else if (cmd === 'render') { + return { gistToken: token, cmd: 'render' } as RenderArgv + } else { + console.error("Supported command: render, add") + process.exit() + } +} + +function parseAddArgv(argv: string[], token: string): AddArgv { + const browserArgv = argv[1] + const fileName = argv[2] + if (!browserArgv || !fileName) { + exitWith("add [c/e/f] [file_name]") + } + const browserArgvMap: Record = { + c: 'chrome', + e: 'edge', + f: 'firefox', + } + const browser: Browser = browserArgvMap[browserArgv] + if (!browser) { + exitWith("add [c/e/f] [file_name]") + } + return { + cmd: 'add', + gistToken: token, + browser, + fileName + } +} \ No newline at end of file diff --git a/script/user-chart/common.ts b/script/user-chart/common.ts new file mode 100644 index 000000000..a81e272b8 --- /dev/null +++ b/script/user-chart/common.ts @@ -0,0 +1,25 @@ +import { findTarget, Gist } from "@api/gist" +import { Browser } from "./argv" + +export function exitWith(msg: string) { + console.error(msg) + process.exit() +} + +/** + * Calculate the gist description of target browser + */ +export function descriptionOf(browser: Browser): string { + return `Timer_UserCount_4_${browser}` +} + +/** + * Calculate the gist filename of target browser + */ +export function filenameOf(browser: Browser): string { + return descriptionOf(browser) + '.json' +} + +export async function getExistGist(token: string, browser: Browser): Promise { + return await findTarget(token, gist => gist.description === descriptionOf(browser)) +} \ No newline at end of file diff --git a/script/user-chart/index.ts b/script/user-chart/index.ts new file mode 100644 index 000000000..8b2ae1b94 --- /dev/null +++ b/script/user-chart/index.ts @@ -0,0 +1,20 @@ +import { add } from "./add" +import { parseArgv } from "./argv" +import { exitWith } from "./common" +import { render } from "./render" + +function main() { + const argv = parseArgv() + switch (argv.cmd) { + case 'add': + add(argv) + break + case 'render': + render(argv) + break + default: + exitWith('Unsupported cmd: ' + JSON.stringify(argv)) + } +} + +main() \ No newline at end of file diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts new file mode 100644 index 000000000..c1d4e02a1 --- /dev/null +++ b/script/user-chart/render.ts @@ -0,0 +1,212 @@ +import { + createGist, + FileForm, + findTarget, + getJsonFileContent, + Gist, + GistForm, + updateGist +} from "@api/gist" +import { UserCount } from "./add" +import { Argv, Browser } from "./argv" +import { filenameOf, getExistGist } from "./common" +import { EChartsType, init } from "echarts" + +const ALL_BROWSERS: Browser[] = ['firefox', 'edge', 'chrome'] + +type OriginData = { + [browser in Browser]: UserCount +} + +type ChartData = { + xAixs: string[] + yAixses: { + [browser in Browser]: number[] + } +} + +export async function render(argv: Argv): Promise { + const token = argv.gistToken + // 1. get all data + const originData: OriginData = await getOriginData(token) + // 2. pre-process data + const chartData = preProcess(originData) + // 3. render csv + const svg = render2Svg(chartData) + // 4. upload + await upload2Gist(token, svg) + process.exit() +} + +function preProcess(originData: OriginData): ChartData { + // 1. sort datess + const dateSet = new Set() + Object.values(originData).forEach(ud => Object.keys(ud).forEach(date => dateSet.add(date))) + let allDates = Array.from(dateSet).sort() + + // 2. smooth the count + const ctx: { [browser in Browser]: SmoothContext } = { + chrome: new SmoothContext(), + firefox: new SmoothContext(), + edge: new SmoothContext(), + } + + allDates.forEach( + date => ALL_BROWSERS.forEach(b => ctx[b].process(originData[b][date])) + ) + const result = { + xAixs: allDates, + yAixses: { + chrome: ctx.chrome.end(), + firefox: ctx.firefox.end(), + edge: ctx.edge.end(), + } + } + + // 3. zoom + const reduction = Math.floor(Object.keys(allDates).length / 150) + result.xAixs = zoom(result.xAixs, reduction) + ALL_BROWSERS.forEach(b => result.yAixses[b] = zoom(result.yAixses[b], reduction)) + return result +} + +class SmoothContext { + lastVal: number + step: number + data: number[] + + constructor() { + this.lastVal = 0 + this.step = 0 + this.data = [] + } + + /** + * Process value + */ + process(newVal: number | undefined) { + if (newVal) { + this.smooth(newVal) + } else { + this.increaseStep() + } + } + + smooth(currentValue: number): void { + if (this.step < 0) { + return + } + const unitVal = (currentValue - this.lastVal) / (this.step + 1) + Object.keys(Array.from(new Array(this.step))) + .map(key => parseInt(key)) + .map(i => Math.floor(unitVal * (i + 1) + this.lastVal)) + .forEach(smoothedVal => this.data.push(smoothedVal)) + this.data.push(currentValue) + // Reset + this.lastVal = currentValue + this.step = 0 + } + + increaseStep(): void { + this.step += 1 + } + + end(): number[] { + Object.keys(new Array(this.step)) + .forEach(() => this.data.push(this.lastVal)) + return this.data + } +} + +function zoom(data: T[], reduction: number): T[] { + let i = 0 + const newData = [] + while (i < data.length) { + newData.push(data[i]) + i += reduction + } + return newData +} + +function render2Svg(chartData: ChartData): string { + const { xAixs, yAixses } = chartData + const chart: EChartsType = init(null, null, { + renderer: 'svg', + ssr: true, + width: 960, + height: 640 + }) + chart.setOption({ + title: { + text: 'Total Active User Count', + subtext: `${xAixs[0]} to ${xAixs[xAixs.length - 1]}` + }, + legend: { data: ALL_BROWSERS }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: [{ + type: 'category', + boundaryGap: false, + data: xAixs + }], + yAxis: [ + { type: 'value' } + ], + series: ALL_BROWSERS.map(b => ({ + name: b, + type: 'line', + stack: 'Total', + // Fill the area + areaStyle: {}, + data: yAixses[b] + })) + }) + return chart.renderToSVGString() +} + +const USER_COUNT_GIST_DESC = "User count of timer, auto-generated" +const USER_COUNT_SVG_FILE_NAME = "user_count.svg" + +async function getOriginData(token: string): Promise { + const [firefox, edge, chrome]: UserCount[] = await Promise.all( + ALL_BROWSERS.map(b => getDataFromGist(token, b)) + ) + return { chrome, firefox, edge } +} + +/** + * Get the data from gist + */ +async function getDataFromGist(token: string, browser: Browser): Promise { + const gist: Gist = await getExistGist(token, browser) + const file = gist?.files[filenameOf(browser)] + return file ? getJsonFileContent(file) : {} +} + +/** + * Upload svg string to gist + */ +async function upload2Gist(token: string, svg: string) { + const files: Record = {} + files[USER_COUNT_SVG_FILE_NAME] = { + filename: USER_COUNT_SVG_FILE_NAME, + content: svg + } + const form: GistForm = { + public: true, + description: USER_COUNT_GIST_DESC, + files + } + const gist = await findTarget(token, gist => gist.description === USER_COUNT_GIST_DESC) + if (gist) { + await updateGist(token, gist.id, form) + console.log('Updated gist') + } else { + await createGist(token, form) + console.log('Created new gist') + } +} \ No newline at end of file diff --git a/script/user_count.py b/script/user_count.py deleted file mode 100644 index 19c429989..000000000 --- a/script/user_count.py +++ /dev/null @@ -1,316 +0,0 @@ -import pygal -from collections import OrderedDict -import sys -import json -import os -import cairosvg -import requests -import math - -svg_file_path = os.path.join('output', 'user_count.svg') -output_dir_path = 'output' - -def smooth_count(last_value, step_num, current_value, data): - unit_val = (current_value-last_value) / (step_num+1) - for i in range(step_num): - data.append(unit_val*(i+1)+last_value) - data.append(current_value) - - -def quit_with(msg: str): - print(msg) - quit() - - -def zoom(data, reduction): - i = 0 - new_data = [] - while i < len(data): - new_data.append(data[i]) - i += reduction - return new_data - - -def render(): - edge_user = read_json('user_edge') - chrome_user = read_json('user_chrome') - firefox_user = read_json('user_firefox') - dates = set() - for date in edge_user: - dates.add(date) - for date in chrome_user: - dates.add(date) - for date in firefox_user: - dates.add(date) - sorted_date = sorted(dates) - last_edge, last_chrome, last_firefox = 0, 0, 0 - edge_step, chrome_step, firefox_step = 0, 0, 0 - - edge_data = [] - chrome_data = [] - firefox_data = [] - for date in sorted_date: - if date in chrome_user: - val = chrome_user[date] - smooth_count(last_chrome, chrome_step, val, chrome_data) - last_chrome = val - chrome_step = 0 - else: - chrome_step += 1 - if date in edge_user: - val = edge_user[date] - smooth_count(last_edge, edge_step, val, edge_data) - last_edge = val - edge_step = 0 - else: - edge_step += 1 - if date in firefox_user: - val = firefox_user[date] - smooth_count(last_firefox, firefox_step, val, firefox_data) - last_firefox = val - firefox_step = 0 - else: - firefox_step += 1 - smooth_count(last_chrome, chrome_step, last_chrome, chrome_data) - smooth_count(last_edge, edge_step, last_edge, edge_data) - smooth_count(last_firefox, firefox_step, last_firefox, firefox_data) - - data_size = len(chrome_data) - reduction = math.floor(data_size/150) - chrome_data = zoom(chrome_data, reduction) - edge_data = zoom(edge_data, reduction) - firefox_data = zoom(firefox_data, reduction) - sorted_date = zoom(sorted_date, reduction) - - chart = pygal.StackedLine( - fill=True, style=pygal.style.styles['default'](label_font_size=8)) - chart.title = 'Active User Count / {} to {}'.format( - sorted_date[0], sorted_date[-1]) - - chart.add('Firefox', firefox_data) - chart.add('Chrome', chrome_data) - chart.add('Edge', edge_data) - svg = chart.render() - if not os.path.exists(output_dir_path): - os.makedirs(output_dir_path) - with open(svg_file_path, 'wb') as svg_file: - svg_file.write(svg) - cairosvg.svg2svg(file_obj=open(svg_file_path, 'r'), write_to=svg_file_path) - - -def read_json(name): - dir_path = 'data' - if not os.path.exists(dir_path): - os.mkdir(dir_path) - return None - file_path = os.path.join(dir_path, '{}.json'.format(name)) - if not os.path.exists(file_path): - return None - with open(file_path, 'r', encoding='utf8') as json_file: - content = '\n'.join(json_file.readlines()) - return json.loads(content) - - -def write_json(name, json_obj): - dir_path = 'data' - if not os.path.exists(dir_path): - os.mkdir(dir_path) - file_path = os.path.join(dir_path, '{}.json'.format(name)) - with open(file_path, 'w', encoding='utf8') as json_file: - json_file.write(json.dumps(json_obj, indent=4)) - - -def add_chrome(file_path): - json_file = 'user_chrome' - if not os.path.exists(file_path): - quit_with("File not found: {}".format(file_path)) - # 1. parse input data - input_data = {} - with open(file_path, encoding='utf8', mode='r') as csv_file: - lines = csv_file.readlines()[2:] - for line in lines: - if not line: - continue - splits = line.split(',') - origin_date = splits[0] - value = int(splits[1]) - date = '-'.join(map(lambda a: a.rjust(2, '0'), - origin_date.split('/'))) - input_data[date] = value - # 2. read exist data - exist_data = read_json(json_file) - if not exist_data: - exist_data = {} - # 3. migrate - for key, val in input_data.items(): - if key not in exist_data and val: - exist_data[key] = val - # 4. write with sorted by key - write_json(json_file, OrderedDict(sorted(exist_data.items()))) - pass - - -def add_edge(file_path): - json_file = 'user_edge' - if not os.path.exists(file_path): - quit_with("File not found: {}".format(file_path)) - # 1. parse input data - input_data = {} - with open(file_path, encoding='utf8', mode='r') as csv_file: - lines = csv_file.readlines()[1:] - for line in lines: - if not line: - continue - splits = line.split(',') - date = splits[5] - value = int(splits[6]) - input_data[date] = value - if not value and len(input_data): - # The last line maybe zero caused by edge's bug - continue - # 2. read exist data - exist_data = read_json(json_file) - if not exist_data: - exist_data = {} - # 3. migrate - for key in input_data: - if key not in exist_data: - exist_data[key] = input_data[key] - # 4. write with sorted by key - write_json(json_file, OrderedDict(sorted(exist_data.items()))) - pass - - -def add_firefox(file_path): - json_file = 'user_firefox' - if not os.path.exists(file_path): - quit_with("File not found: {}".format(file_path)) - # 1. parse input data - input_data = {} - with open(file_path, encoding='utf8', mode='r') as csv_file: - lines = csv_file.readlines()[4:] - for line in lines: - if not line: - continue - splits = line.split(',') - date = splits[0] - value = int(splits[1]) - input_data[date] = value - # 2. read exist data - exist_data = read_json(json_file) - if not exist_data: - exist_data = {} - # 3. migrate - for key in input_data: - if key not in exist_data: - exist_data[key] = input_data[key] - # 4. write with sorted by key - write_json(json_file, OrderedDict(sorted(exist_data.items()))) - pass - - -def add(): - argv = sys.argv - if len(argv) < 4 or argv[2] not in ['c', 'e', 'f']: - quit_with("add [c/e/f] [file_name]") - browser = argv[2] - file_path = argv[3] - if browser == 'c': - add_chrome(file_path) - elif browser == 'e': - add_edge(file_path) - elif browser == 'f': - add_firefox(file_path) - else: - pass - - -GIST_TOKEN_ENV = 'TIMER_USER_COUNT_GIST_TOKEN' -DESC = "User count of timer, auto-generated" - - -def upload2gist(): - argv = sys.argv - # 1. find token and svg file - token = argv[2] if len(argv) > 2 else os.environ.get(GIST_TOKEN_ENV) - if not token: - quit_with( - "Token is None, please input with command or set with environment TIMER_USER_COUNT_GIST_TOKEN" - ) - if not os.path.exists(svg_file_path): - quit_with('Svg file not found') - # 2. find exist gist file - token_header = 'Bearer {}'.format(token) - headers = { - "Accept": "application/vnd.github+json", - "Authorization": token_header - } - response = requests.get('https://api.github.com/gists', headers=headers) - status_code = response.status_code - if status_code != 200: - quit_with("Failed to list exist gists: statusCode={}".format(status_code)) - gist_list = json.loads(response.content) - exist_gist = next( - (gist for gist in gist_list if 'description' in gist and gist['description'] == DESC), - None - ) - svg_content = '' - with open(svg_file_path, 'r') as file: - svg_content = '\r\n'.join(file.readlines()) - if not svg_content: - quit_with("Failed to read svg file") - data = json.dumps({ - "description": DESC, - "public": True, - "files": { - "user_count.svg": { - "content": svg_content - } - } - }) - # 3. create new one or update - if not exist_gist: - print("Gist not found, so try to create one") - response = requests.post('https://api.github.com/gists', - data=data, headers=headers) - status_code = response.status_code - if status_code != 200 and status_code != 201: - quit_with( - 'Failed to create new gist: statusCode={}'.format(status_code) - ) - else: - print('Success to create new gist') - else: - gist_id = exist_gist['id'] - response = requests.post('https://api.github.com/gists/{}'.format(gist_id), - data=data, headers=headers) - status_code = response.status_code - if status_code != 200 and status_code != 201: - quit_with("Failed to update gist: id={}, statusCode={}".format( - gist_id, - status_code - )) - else: - print("Success to update gist") - - -def main(): - argv = sys.argv - if len(argv) == 1 or argv[1] not in ['render', 'add', 'upload']: - print("Supported command: render, add, upload") - quit() - - cmd = argv[1] - if cmd == 'render': - render() - elif cmd == 'add': - add() - elif cmd == 'upload': - render() - upload2gist() - else: - pass - - -if __name__ == '__main__': - main() diff --git a/tsconfig.json b/tsconfig.json index 27777ea1e..af9f1dbc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,5 +51,11 @@ "node_modules", "dist" ], - "strict": true + "strict": true, + "ts-node": { + "files": true, + "require": [ + "tsconfig-paths/register" + ] + } } \ No newline at end of file From 7535b143aa5c2279faba7c848d8a416515e509de Mon Sep 17 00:00:00 2001 From: ZHY Date: Thu, 15 Dec 2022 19:38:31 +0800 Subject: [PATCH 057/168] Fix smooth error --- script/user-chart/render.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index c1d4e02a1..34ebe1ca0 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -112,7 +112,7 @@ class SmoothContext { } end(): number[] { - Object.keys(new Array(this.step)) + Object.keys(Array.from(new Array(this.step))) .forEach(() => this.data.push(this.lastVal)) return this.data } @@ -209,4 +209,4 @@ async function upload2Gist(token: string, svg: string) { await createGist(token, form) console.log('Created new gist') } -} \ No newline at end of file +} From 5329bc43ab0f9465a834fda97cbeb59f5e52b09e Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 9 Dec 2022 21:42:15 +0800 Subject: [PATCH 058/168] Integrate with Crowdin (#96) --- global.d.ts | 28 +- package.json | 1 + script/crowdin/client.ts | 338 ++++++++++++++++++ script/crowdin/common.ts | 226 ++++++++++++ script/crowdin/crowdin.d.ts | 39 ++ script/crowdin/export-translation.ts | 31 ++ script/crowdin/sync-source.ts | 95 +++++ script/crowdin/sync-translation.ts | 73 ++++ script/user-chart/add.ts | 3 +- script/user-chart/argv.ts | 2 +- script/user-chart/common.ts | 5 - script/user-chart/index.ts | 2 +- script/util/process.ts | 5 + src/api/crowdin.ts | 36 ++ .../components/common/switch-filter-item.ts | 1 + .../dashboard/components/calendar-heat-map.ts | 2 +- .../dashboard/components/indicator/style.sass | 1 + .../dashboard/components/week-on-week.ts | 1 + .../data-manage/clear/filter/date-filter.ts | 2 +- .../data-manage/clear/filter/number-filter.ts | 2 +- .../clear/filter/operation-button.ts | 4 +- .../components/data-manage/style/index.sass | 1 + src/app/components/habit/component/filter.ts | 2 +- src/app/components/help-us/alert-info.ts | 25 ++ src/app/components/help-us/index.ts | 21 ++ src/app/components/help-us/progress-list.ts | 85 +++++ src/app/components/help-us/style.sass | 27 ++ src/app/components/help-us/toolbar.ts | 33 ++ src/app/components/option/common.ts | 2 +- .../components/appearance/dark-mode-input.ts | 1 + .../option/components/appearance/index.ts | 6 +- src/app/components/option/components/popup.ts | 6 +- src/app/components/report/filter/index.ts | 3 +- .../report/table/columns/operation.ts | 2 +- src/app/components/rule-merge/alert-info.ts | 18 +- src/app/components/rule-merge/index.ts | 10 +- src/app/components/trend/components/filter.ts | 3 +- src/app/components/whitelist/alert-info.ts | 2 +- src/app/components/whitelist/index.ts | 16 +- src/app/index.ts | 2 +- src/app/layout/menu.ts | 33 +- src/app/{locale/index.ts => locale.ts} | 12 +- src/app/locale/components/site-manage.ts | 179 ---------- src/app/router/index.ts | 11 +- src/background/backup/gist/coordinator.ts | 1 + src/background/browser-action-menu-manager.ts | 6 +- src/background/uninstall-listener.ts | 2 +- .../0-7-0/local-file-initializer.ts | 2 +- src/background/whitelist-menu-manager.ts | 4 +- src/content-script/index.ts | 2 +- src/content-script/limit.ts | 4 +- src/content-script/printer.ts | 2 +- src/echarts/chart/bar.ts | 1 + src/echarts/chart/candlestick.ts | 1 + src/echarts/chart/heatmap.ts | 1 + src/echarts/chart/line.ts | 1 + src/echarts/chart/pie.ts | 1 + src/echarts/component/grid.ts | 1 + src/echarts/component/legend.ts | 1 + src/echarts/component/title.ts | 1 + src/echarts/component/toolbox.ts | 1 + src/echarts/component/tooltip.ts | 1 + src/echarts/component/visual-map.ts | 1 + src/guide/component/common.ts | 4 +- src/guide/component/profile.ts | 4 +- src/guide/component/usage.ts | 4 +- src/guide/index.ts | 5 +- src/guide/layout/menu.ts | 2 +- src/guide/{locale/index.ts => locale.ts} | 10 +- src/guide/locale/components/profile.ts | 39 -- src/{util => }/i18n/chrome/compile.ts | 0 src/{util => }/i18n/chrome/index.ts | 0 src/{util => }/i18n/chrome/message.ts | 32 +- src/{util => }/i18n/chrome/t.ts | 0 src/{util => }/i18n/i18n-vue.ts | 2 +- src/i18n/i18n.d.ts | 4 + src/{util => }/i18n/index.ts | 20 +- .../message/app}/confirm.ts | 7 +- .../message/app}/dashboard.ts | 6 +- .../message/app}/data-manage.ts | 40 +-- .../components => i18n/message/app}/habit.ts | 44 ++- src/i18n/message/app/help-us.ts | 83 +++++ .../messages.ts => i18n/message/app/index.ts} | 47 ++- .../components => i18n/message/app}/limit.ts | 55 ++- .../components => i18n/message/app}/menu.ts | 15 +- .../message/app}/merge-rule.ts | 15 +- .../message/app}/operation.ts | 16 +- .../components => i18n/message/app}/option.ts | 195 +++++----- .../components => i18n/message/app}/report.ts | 16 +- src/i18n/message/app/site-manage.ts | 172 +++++++++ .../message/app}/time-format.ts | 12 +- .../components => i18n/message/app}/trend.ts | 20 +- .../message/app}/whitelist.ts | 11 +- .../app.ts => i18n/message/common/base.ts} | 23 +- .../message/common}/calendar.ts | 6 +- .../message/common}/content-script.ts | 12 +- .../message/common}/context-menus.ts | 15 +- .../message/common}/initial.ts | 12 +- .../message/common}/item.ts | 20 +- src/i18n/message/common/locale.ts | 33 ++ src/i18n/message/common/meta.ts | 31 ++ src/i18n/message/common/popup-duration.ts | 33 ++ .../message/guide/index.ts} | 21 +- .../message/guide}/layout.ts | 30 +- .../message/guide}/privacy.ts | 38 +- src/i18n/message/guide/profile.ts | 35 ++ .../message/guide}/usage.ts | 81 ++--- .../message/popup/chart.ts} | 61 +--- src/i18n/message/popup/index.ts | 53 +++ src/package.ts | 1 + src/popup/components/chart/option.ts | 16 +- src/popup/components/footer/all-function.ts | 3 +- src/popup/components/footer/index.ts | 8 +- src/popup/components/footer/merge-host.ts | 2 +- .../components/footer/select/time-select.ts | 4 +- src/popup/components/footer/total-info.ts | 4 +- src/popup/components/footer/upgrade.ts | 6 +- src/popup/index.ts | 3 +- src/popup/{locale/index.ts => locale.ts} | 6 +- src/popup/style/index.sass | 1 + src/util/array.ts | 1 + src/util/constant/url.ts | 15 + src/util/fifo-cache.ts | 1 + src/util/i18n/components/locale.ts | 34 -- src/util/i18n/components/popup-duration.ts | 35 -- test/util/chrome/compile.test.ts | 2 +- tsconfig.json | 6 + webpack/webpack.common.ts | 4 +- 128 files changed, 2065 insertions(+), 902 deletions(-) create mode 100644 script/crowdin/client.ts create mode 100644 script/crowdin/common.ts create mode 100644 script/crowdin/crowdin.d.ts create mode 100644 script/crowdin/export-translation.ts create mode 100644 script/crowdin/sync-source.ts create mode 100644 script/crowdin/sync-translation.ts create mode 100644 script/util/process.ts create mode 100644 src/api/crowdin.ts create mode 100644 src/app/components/help-us/alert-info.ts create mode 100644 src/app/components/help-us/index.ts create mode 100644 src/app/components/help-us/progress-list.ts create mode 100644 src/app/components/help-us/style.sass create mode 100644 src/app/components/help-us/toolbar.ts rename src/app/{locale/index.ts => locale.ts} (57%) delete mode 100644 src/app/locale/components/site-manage.ts rename src/guide/{locale/index.ts => locale.ts} (56%) delete mode 100644 src/guide/locale/components/profile.ts rename src/{util => }/i18n/chrome/compile.ts (100%) rename src/{util => }/i18n/chrome/index.ts (100%) rename src/{util => }/i18n/chrome/message.ts (76%) rename src/{util => }/i18n/chrome/t.ts (100%) rename src/{util => }/i18n/i18n-vue.ts (91%) create mode 100644 src/i18n/i18n.d.ts rename src/{util => }/i18n/index.ts (83%) rename src/{app/locale/components => i18n/message/app}/confirm.ts (89%) rename src/{app/locale/components => i18n/message/app}/dashboard.ts (98%) rename src/{app/locale/components => i18n/message/app}/data-manage.ts (91%) rename src/{app/locale/components => i18n/message/app}/habit.ts (80%) create mode 100644 src/i18n/message/app/help-us.ts rename src/{app/locale/messages.ts => i18n/message/app/index.ts} (66%) rename src/{app/locale/components => i18n/message/app}/limit.ts (83%) rename src/{app/locale/components => i18n/message/app}/menu.ts (90%) rename src/{app/locale/components => i18n/message/app}/merge-rule.ts (92%) rename src/{app/locale/components => i18n/message/app}/operation.ts (78%) rename src/{app/locale/components => i18n/message/app}/option.ts (74%) rename src/{app/locale/components => i18n/message/app}/report.ts (97%) create mode 100644 src/i18n/message/app/site-manage.ts rename src/{app/locale/components => i18n/message/app}/time-format.ts (81%) rename src/{app/locale/components => i18n/message/app}/trend.ts (92%) rename src/{app/locale/components => i18n/message/app}/whitelist.ts (92%) rename src/{util/i18n/components/app.ts => i18n/message/common/base.ts} (51%) rename src/{util/i18n/components => i18n/message/common}/calendar.ts (91%) rename src/{util/i18n/components => i18n/message/common}/content-script.ts (92%) rename src/{util/i18n/components => i18n/message/common}/context-menus.ts (73%) rename src/{util/i18n/components => i18n/message/common}/initial.ts (92%) rename src/{util/i18n/components => i18n/message/common}/item.ts (94%) create mode 100644 src/i18n/message/common/locale.ts create mode 100644 src/i18n/message/common/meta.ts create mode 100644 src/i18n/message/common/popup-duration.ts rename src/{guide/locale/messages.ts => i18n/message/guide/index.ts} (60%) rename src/{guide/locale/components => i18n/message/guide}/layout.ts (74%) rename src/{guide/locale/components => i18n/message/guide}/privacy.ts (74%) create mode 100644 src/i18n/message/guide/profile.ts rename src/{guide/locale/components => i18n/message/guide}/usage.ts (77%) rename src/{popup/locale/messages.ts => i18n/message/popup/chart.ts} (64%) create mode 100644 src/i18n/message/popup/index.ts rename src/popup/{locale/index.ts => locale.ts} (61%) delete mode 100644 src/util/i18n/components/locale.ts delete mode 100644 src/util/i18n/components/popup-duration.ts diff --git a/global.d.ts b/global.d.ts index b1265520d..87834cf98 100644 --- a/global.d.ts +++ b/global.d.ts @@ -168,16 +168,38 @@ declare namespace timer { } } + /** + * The source locale + * + * @since 1.4.0 + */ + type SourceLocale = 'en' + /** * @since 0.8.0 */ - type Locale = + type Locale = SourceLocale | 'zh_CN' - | 'en' | 'ja' // @since 0.9.0 | 'zh_TW' + /** + * Translating locales + * + * @since 1.4.0 + */ + type TranslatingLocale = + | 'de' + | 'en_GB' + | 'en_US' + | 'es' + | 'ko' + | 'pl' + | 'pt' + | 'pt_BR' + | 'ru' + namespace stat { /** * The dimension to statistics @@ -564,4 +586,4 @@ declare namespace timer { */ type Callback = (result?: Response) => void } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 5c5a779be..e7ccd66b1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "author": "zhy", "license": "MIT", "devDependencies": { + "@crowdin/crowdin-api-client": "^1.19.2", "@types/chrome": "0.0.199", "@types/copy-webpack-plugin": "^8.0.1", "@types/echarts": "^4.9.16", diff --git a/script/crowdin/client.ts b/script/crowdin/client.ts new file mode 100644 index 000000000..ec354a8a2 --- /dev/null +++ b/script/crowdin/client.ts @@ -0,0 +1,338 @@ +import Crowdin, { + Credentials, + Pagination, + PatchRequest, + ResponseList, + SourceFilesModel, + SourceStringsModel, + StringTranslationsModel, + UploadStorageModel, +} from '@crowdin/crowdin-api-client' +import axios from 'axios' + +const PROJECT_ID = 516822 + +const MAIN_BRANCH_NAME = 'main' + +/** + * The iterator of response + */ +class PaginationIterator { + private offset = 0 + private limit = 25 + private isEnd = false + private buf: T[] = [] + private cursor = 0 + private query: (pagination: Pagination) => Promise> + + constructor(query: (pagination: Pagination) => Promise>) { + this.query = query + } + + reset(): void { + this.offset = 0 + this.isEnd = false + this.buf = [] + this.cursor = 0 + } + + async findFirst(predicate: (ele: T) => boolean): Promise { + while (true) { + const data = await this.next() + if (!data) { + break + } + if (data && predicate(data)) { + return data + } + } + return undefined + } + + async findAll(predicate?: ((ele: T) => boolean)): Promise { + const result = [] + while (true) { + const data = await this.next() + if (!data) { + break + } + if (predicate ? predicate(data) : true) { + result.push(data) + } + } + return result + } + + async next(): Promise { + if (this.isEnd) { + return undefined + } + if (this.cursor >= this.buf.length) { + await this.processBuf() + } + if (this.isEnd) { + return undefined + } + return this.buf[this.cursor++] + } + + private async processBuf() { + const pagination: Pagination = { offset: this.offset, limit: this.limit } + const list = await this.query(pagination) + const data = list?.data + if (!data?.length) { + this.isEnd = true + } else { + this.buf = data.map(obj => obj.data) + this.cursor = 0 + this.offset += this.buf.length + } + } +} + +async function createStorage(fileName: string, content: any): Promise { + const response = await this.crowdin.uploadStorageApi.addStorage(fileName, content) + return response.data +} + +async function createFile(this: CrowdinClient, + directoryId: number, + storage: UploadStorageModel.Storage, + fileName: string +): Promise { + const request: SourceFilesModel.CreateFileRequest = { + name: fileName, + storageId: storage.id, + directoryId, + type: 'json', + } + const response = await this.crowdin.sourceFilesApi.createFile(PROJECT_ID, request) + return response.data +} + +async function restoreFile(this: CrowdinClient, storage: UploadStorageModel.Storage, existFile: SourceFilesModel.File): Promise { + const response = await this.crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, existFile.id, { storageId: storage.id }) + return response.data +} + +async function getMainBranch(this: CrowdinClient): Promise { + return new PaginationIterator( + pagination => this.crowdin.sourceFilesApi.listProjectBranches(PROJECT_ID, { ...pagination }) + ).findFirst(e => e.name === MAIN_BRANCH_NAME) +} + +async function createMainBranch(): Promise { + const request: SourceFilesModel.CreateBranchRequest = { + name: MAIN_BRANCH_NAME + } + const res = await this.crowdin.sourceFilesApi.createBranch(PROJECT_ID, request) + return res.data +} + +async function getOrCreateMainBranch(this: CrowdinClient): Promise { + let branch = await this.getMainBranch() + if (!branch) { + branch = await this.createMainBranch() + } + console.info("getOrCreateMainBranch: " + JSON.stringify(branch)) + return branch +} + +function getFileByName(this: CrowdinClient, param: NameKey): Promise { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, branchId: param.branchId }) + ).findFirst(t => t.name === param.name) +} + +function getDirByName(this: CrowdinClient, param: NameKey): Promise { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, { ...p, branchId: param.branchId }) + ).findFirst(d => d.name === param.name) +} + +async function createDirectory(this: CrowdinClient, param: NameKey): Promise { + const res = await this.crowdin.sourceFilesApi.createDirectory(PROJECT_ID, { + name: param.name, + branchId: param.branchId, + }) + return res.data +} + +function listFilesByDirectory(this: CrowdinClient, directoryId: number) { + return new PaginationIterator( + p => this.crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID, { ...p, directoryId: directoryId }) + ).findAll() +} + +function listStringsByFile(this: CrowdinClient, fileId: number): Promise { + return new PaginationIterator( + p => this.crowdin.sourceStringsApi.listProjectStrings(PROJECT_ID, { ...p, fileId: fileId }) + ).findAll() +} + +async function batchCreateString( + this: CrowdinClient, + fileId: number, + content: ItemSet, +): Promise { + for (const [path, value] of Object.entries(content)) { + const request: SourceStringsModel.CreateStringRequest = { + fileId, + text: value, + identifier: path, + } + console.log(`Try to create new string: ${JSON.stringify(request)}`) + await this.crowdin.sourceStringsApi.addString(PROJECT_ID, request) + } +} + +async function batchUpdateIfNecessary(this: CrowdinClient, + content: ItemSet, + existStringsKeyMap: { [path: string]: SourceStringsModel.String } +): Promise { + console.log("=========start to update strings========") + console.log("Content length: " + Object.keys(content).length) + for (const [path, value] of Object.entries(content)) { + const string = existStringsKeyMap[path] + const patch: PatchRequest[] = [] + string?.text !== value && patch.push({ + op: 'replace', + path: '/text', + value: value + }) + if (!patch.length) { + continue + } + console.log('Try to edit string: ' + string.identifier) + await this.crowdin.sourceStringsApi.editString(PROJECT_ID, string.id, patch) + } + console.log("=========end to update strings========") +} + +async function batchDeleteString(this: CrowdinClient, stringIds: number[]): Promise { + console.log("=========start to delete strings========") + for (const stringId of stringIds) { + await this.crowdin.sourceStringsApi.deleteString(PROJECT_ID, stringId) + console.log("Delete string: id=" + stringId) + } + console.log("=========end to delete strings========") +} + +async function listAllTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise { + const { stringId, lang } = transKey + return new PaginationIterator( + p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p }) + ).findAll() +} + +async function existTranslationByStringAndLang(this: CrowdinClient, transKey: TranslationKey): Promise { + const { stringId, lang } = transKey + const trans = await new PaginationIterator( + p => this.crowdin.stringTranslationsApi.listStringTranslations(PROJECT_ID, stringId, lang, { ...p }) + ).findFirst(_ => true) + return !!trans +} + +async function createTranslation(this: CrowdinClient, transKey: TranslationKey, text: string) { + const { stringId, lang } = transKey + const request: StringTranslationsModel.AddStringTranslationRequest = { + stringId, + languageId: lang, + text + } + 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, { + targetLanguageId: lang, + fileIds: [fileId], + format: 'android', + }) + 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 +} + +/** + * The wrapper of client with auth + */ +export class CrowdinClient { + crowdin: Crowdin + + constructor(token: string) { + const credentials: Credentials = { + token: token + } + this.crowdin = new Crowdin(credentials) + console.info("Intialized client successfully") + } + createStorage = createStorage + /** + * Get the main branch + * + * @returns main branch or undefined + */ + getMainBranch = getMainBranch + + /** + * Create the main branch + */ + createMainBranch = createMainBranch + + getOrCreateMainBranch = getOrCreateMainBranch + + createFile = createFile + + restoreFile = restoreFile + + getFileByName = getFileByName + + getDirByName = getDirByName + + createDirectory = createDirectory + + listFilesByDirectory = listFilesByDirectory + + listStringsByFile = listStringsByFile + + batchCreateString = batchCreateString + + batchUpdateIfNecessary = batchUpdateIfNecessary + + batchDeleteString = batchDeleteString + + listAllTranslationByStringAndLang = listAllTranslationByStringAndLang + + existTranslationByStringAndLang = existTranslationByStringAndLang + + createTranslation = createTranslation + + downloadTranslations = downloadTranslations +} + +/** + * Get the client from environment variable [TIMER_CROWDIN_AUTH] + * + * @returns client + */ +export function getClientFromEnv(): CrowdinClient { + const envVar = process.env?.TIMER_CROWDIN_AUTH + if (!envVar) { + console.error("Failed to get the variable named [TIMER_CROWDIN_AUTH]") + process.exit(1) + } + return new CrowdinClient(envVar) +} diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts new file mode 100644 index 000000000..1f5dc232c --- /dev/null +++ b/script/crowdin/common.ts @@ -0,0 +1,226 @@ +import path from 'path' +import fs from 'fs' +import { CrowdinClient } from './client' +import { exitWith } from '../util/process' + +export const ALL_DIRS: Dir[] = ['app', 'common', 'popup', 'guide'] + +export const SOURCE_LOCALE: timer.SourceLocale = 'en' + +// Not include en and zh_CN +export const ALL_TRANS_LOCALES: timer.Locale[] = [ + 'ja', + 'zh_TW', +] + +const CROWDIN_I18N_MAP: Record = { + en: 'en', + ja: 'ja', + "zh-CN": 'zh_CN', + "zh-TW": 'zh_TW', +} + +const I18N_CROWDIN_MAP: Record = { + en: 'en', + ja: 'ja', + zh_CN: 'zh-CN', + zh_TW: 'zh-TW', +} + +export const crowdinLangOf = (locale: timer.Locale) => I18N_CROWDIN_MAP[locale] + +export const localeOf = (crowdinLang: CrowdinLanguage) => CROWDIN_I18N_MAP[crowdinLang] + +const IGNORED_FILE: Partial<{ [dir in Dir]: string[] }> = { + common: [ + // Strings for market + 'meta.ts', + // Name of locales + 'locale.ts', + ] +} + +export function isIgnored(dir: Dir, tsFilename: string) { + return !!IGNORED_FILE[dir]?.includes(tsFilename) +} + +const MSG_BASE = path.join(__dirname, '..', '..', 'src', 'i18n', 'message') + +/** + * Read all messages from source file + * + * @param dir the directory of messages + * @returns + */ +export async function readAllMessages(dir: Dir): Promise>> { + const dirPath = path.join(MSG_BASE, dir) + + const files = fs.readdirSync(dirPath) + const result = {} + await Promise.all(files.map(async file => { + if (!file.endsWith('.ts')) { + return + } + if (file === 'index.ts') { + return + } + const message = (await import(`@i18n/message/${dir}/${file}`))?.default as Messages + message && (result[file] = message) + return + })) + return result +} + + +/** + * Merge crowdin message into locale codes + */ +export async function mergeMessage( + dir: Dir, + filename: string, + messages: Partial> +): Promise { + const dirPath = path.join(MSG_BASE, dir) + const filePath = path.join(dirPath, filename) + const existMessages = (await import(`@i18n/message/${dir}/${filename}`))?.default as Messages + if (!existMessages) { + console.error(`Failed to find local code: dir=${dir}, filename=${filename}`) + return + } + const sourceItemSet = transMsg(existMessages[SOURCE_LOCALE]) + Object.entries(messages).forEach(([locale, itemSet]) => { + let existMessage: any = existMessages[locale] + if (!existMessage) { + existMessages[locale] = existMessage = {} + } + Object.entries(itemSet).forEach(([path, text]) => { + const sourceText = sourceItemSet[path] + if (!checkPlaceholder(text, sourceText)) { + console.error(`Invalid placeholder: dir=${dir}, filename=${filename}, path=${path}, source=${sourceText}, translated=${text}`) + return + } + const pathSeg = path.split('.') + fillItem(pathSeg, 0, existMessage, text) + }) + }) + + const existFile = fs.readFileSync(filePath, { encoding: 'utf-8' }) + const pattern = /(const|let|var) _default(.*)=\s*\{\s*(\n?.*\n)+\}/ + const patternRes = pattern.exec(existFile) + const existDefault = patternRes?.[0] + if (!existDefault) { + exitWith(`Failed to find: ${pattern} in ${filePath}`) + } + const index = existFile.indexOf(existDefault) + const pre = existFile.substring(0, index) + const suffix = existFile.substring(index + existDefault.length) + const newDefault = generateDefault(existDefault, existMessages) + const newFileContent = pre + newDefault + suffix + fs.writeFileSync(filePath, newFileContent, { encoding: 'utf-8' }) +} + +function checkPlaceholder(translated: string, source: string) { + const allSourcePlaceholders = + Array.from(source.matchAll(/\{(.*?)\}/g)) + .map(matched => matched[1]) + .sort() + const allTranslatedPlaceholders = + Array.from(translated.matchAll(/\{(.*?)\}/g)) + .map(matched => matched[1]) + .sort() + if (allSourcePlaceholders.length != allTranslatedPlaceholders.length) { + return false + } + for (let i = 0; i++; i < allSourcePlaceholders.length) { + if (allSourcePlaceholders[i] !== allTranslatedPlaceholders[i]) { + return false + } + } + return true +} + +const INDENTATION_UNIT = ' ' + +function generateDefault(existDetault: string, messages: Messages): string { + let codeLines = /(const|let|var) _default(.*)=\s*\{/.exec(existDetault)[0] + codeLines += '\n' + codeLines += generateFieldLines(messages, INDENTATION_UNIT) + codeLines += '\n}' + return codeLines +} + +function generateFieldLines(message: Object, indentation: string): string { + const lines = [] + Object.entries(message).forEach(([key, value]) => { + let line = undefined + if (typeof value === 'object') { + const subCodeLines = generateFieldLines(value, indentation + INDENTATION_UNIT) + line = `${indentation}${key}: {\n${subCodeLines}\n${indentation}}` + } else { + const valueText = JSON.stringify(value) + // Use double quotes + .replace(/'/g, '\\\'').replace(/"/g, '\'') + line = `${indentation}${key}: ${valueText}` + } + lines.push(line) + }) + let codeLines = lines.join(',\n') + if (codeLines) { + // Add comma at the end of last line + codeLines += ',' + } + return codeLines +} + + +function fillItem(fields: string[], index: number, obj: Object, text: string) { + const field = fields[index] + if (index === fields.length - 1) { + obj[field] = text + return + } + let sub = obj[field] + if (sub === undefined) { + obj[field] = sub = {} + } else if (typeof sub !== 'object') { + exitWith("Invalid key path: " + fields.join('.')) + } + fillItem(fields, index + 1, sub, text) +} + +/** + * Trans msg object to k-v map + */ +export function transMsg(message: any, prefix?: string): ItemSet { + const result = {} + const pathPrefix = prefix ? prefix + '.' : '' + Object.entries(message).forEach(([key, value]) => { + const path = pathPrefix + key + if (typeof value === 'object') { + const subResult = transMsg(value, path) + Object.entries(subResult) + .forEach(([path, val]) => result[path] = val) + } else { + result[path] = value + } + }) + return result +} + +export async function checkMainBranch(client: CrowdinClient) { + const branch = await client.getMainBranch() + if (!branch) { + exitWith("Main branch is null") + } + 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/crowdin.d.ts b/script/crowdin/crowdin.d.ts new file mode 100644 index 000000000..e887914e6 --- /dev/null +++ b/script/crowdin/crowdin.d.ts @@ -0,0 +1,39 @@ +/** + * The directory of messages + */ +type Dir = + | 'app' + | 'common' + | 'guide' + | 'popup' + +/** + * Key of crowdin file/directory + */ +type NameKey = { + name: Dir + branchId: number +} + +type TranslationKey = { + stringId: number + lang: CrowdinLanguage +} + +/** + * Items of message + */ +type ItemSet = { + [path: string]: string +} + +/** + * The language code of crowdin + * + * @see https://developer.crowdin.com/language-codes/ + */ +type CrowdinLanguage = + | 'zh-CN' + | 'en' + | 'zh-TW' + | 'ja' \ No newline at end of file diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts new file mode 100644 index 000000000..b2b08df91 --- /dev/null +++ b/script/crowdin/export-translation.ts @@ -0,0 +1,31 @@ +import { SourceFilesModel } from "@crowdin/crowdin-api-client" +import { CrowdinClient, getClientFromEnv } from "./client" +import { ALL_DIRS, ALL_TRANS_LOCALES, checkMainBranch, crowdinLangOf, mergeMessage } from "./common" + +async function processFile(client: CrowdinClient, file: SourceFilesModel.File, dir: Dir): Promise { + const itemSets: Partial> = {} + for (const locale of ALL_TRANS_LOCALES) { + const lang = crowdinLangOf(locale) + const items: ItemSet = await client.downloadTranslations(file.id, lang) + itemSets[locale] = items + } + await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) +} + +async function processDir(client: CrowdinClient, branch: SourceFilesModel.Branch, dir: Dir): Promise { + const directory = await client.getDirByName({ name: dir, branchId: branch.id }) + const files = await client.listFilesByDirectory(directory.id) + for (const file of files) { + processFile(client, file, dir) + } +} + +async function main() { + const client = getClientFromEnv() + const branch = await checkMainBranch(client) + for (const dir of ALL_DIRS) { + await processDir(client, branch, dir) + } +} + +main() \ No newline at end of file diff --git a/script/crowdin/sync-source.ts b/script/crowdin/sync-source.ts new file mode 100644 index 000000000..e5bbe27b1 --- /dev/null +++ b/script/crowdin/sync-source.ts @@ -0,0 +1,95 @@ +import { SourceFilesModel, SourceStringsModel } from "@crowdin/crowdin-api-client" +import { groupBy } from "@util/array" +import { CrowdinClient, getClientFromEnv } from "./client" +import { ALL_DIRS, isIgnored, readAllMessages, SOURCE_LOCALE, transMsg } from "./common" + +async function initBranch(client: CrowdinClient): Promise { + const branch = await client.getOrCreateMainBranch() + if (!branch) { + console.error("Failed to create main branch") + process.exit(1) + } + return branch +} + +/** + * Process strings + * + * @param client client + * @param existFile exist crowdin file + * @param fileContent strings + */ +async function processStrings( + client: CrowdinClient, + existFile: SourceFilesModel.File, + fileContent: ItemSet, +) { + const existStrings = await client.listStringsByFile(existFile.id) + const existStringsKeyMap = groupBy(existStrings, s => s.identifier, l => l[0]) + const strings2Delete: SourceStringsModel.String[] = [] + const strings2Create: ItemSet = {} + const strings2Update: ItemSet = {} + Object.entries(fileContent).forEach(([path, text]) => { + if (!text) { + // maybe blank or undefined sometimes + return + } + const existString = existStringsKeyMap[path] + if (existString) { + strings2Update[path] = text + } else { + strings2Create[path] = text + } + }) + Object.entries(existStringsKeyMap).forEach(([path, string]) => !fileContent[path] && strings2Delete.push(string)) + await client.batchCreateString(existFile.id, strings2Create) + await client.batchUpdateIfNecessary(strings2Update, existStringsKeyMap) + await client.batchDeleteString(strings2Delete.map(s => s.id)) +} + + +async function processByDir(client: CrowdinClient, dir: Dir, branch: SourceFilesModel.Branch): Promise { + // 1. init directory + const dirKey: NameKey = { name: dir, branchId: branch.id } + let directory = await client.getDirByName(dirKey) + if (!directory) { + directory = await client.createDirectory(dirKey) + } + console.log('Direcotory: ' + JSON.stringify(directory)) + // 2. iterate all messages + const messages = await readAllMessages(dir) + // 3. list all files in directory + const existFiles = await client.listFilesByDirectory(directory.id) + console.log("Exists file count: " + existFiles.length) + const existFileNameMap = groupBy(existFiles, f => f.name, l => l[0]) + // 4. create new files + for (const [tsFilename, msg] of Object.entries(messages)) { + if (isIgnored(dir, tsFilename)) { + console.log(`Ignored file: ${dir}/${tsFilename}`) + continue + } + const crwodinFilename = tsFilename.replace('.ts', '.json') + const fileContent = transMsg(msg[SOURCE_LOCALE]) + let existFile = existFileNameMap[crwodinFilename] + if (!existFile) { + // Create with empty file + const storage = await client.createStorage(crwodinFilename, fileContent) + existFile = await client.createFile(directory.id, storage, crwodinFilename) + console.log(`Created new file: dir=${dir}, fileName=${crwodinFilename}, id=${existFile.id}`) + } + // Process by strings + await processStrings(client, existFile, fileContent) + } +} + +async function main() { + const client = getClientFromEnv() + // Init main branch + const branch = await initBranch(client) + // Process by dir + for (const dir of ALL_DIRS) { + await processByDir(client, dir, branch) + } +} + +main() \ No newline at end of file diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts new file mode 100644 index 000000000..f4f8866e9 --- /dev/null +++ b/script/crowdin/sync-translation.ts @@ -0,0 +1,73 @@ + +import { exitWith } from "../util/process" +import { ALL_DIRS, ALL_TRANS_LOCALES, checkMainBranch, crowdinLangOf, isIgnored, readAllMessages, transMsg } from "./common" +import { CrowdinClient, getClientFromEnv } from "./client" +import { SourceFilesModel } from "@crowdin/crowdin-api-client" +import { groupBy } from "@util/array" + +async function processDirMessage(client: CrowdinClient, file: SourceFilesModel.File, message: ItemSet, lang: CrowdinLanguage): Promise { + const strings = await client.listStringsByFile(file.id) + const stringMap = groupBy(strings, s => s.identifier, l => l[0]) + for (const [identifier, text] of Object.entries(message)) { + const string = stringMap[identifier] + if (!string) { + console.log(`Can't found string of identifier: ${identifier}, file: ${file.path}`) + continue + } + if (text === string.text) { + // The same as original text + console.log(`Translation same as origin text of ${string.identifier} in ${file.path}`) + continue + } + const exist = await client.existTranslationByStringAndLang({ stringId: string.id, lang }) + if (exist) { + // Already exist, not sync + continue + } + await client.createTranslation({ stringId: string.id, lang }, text) + console.log(`Created trans: stringId=${string.id}, lang=${lang}, text=${text}`) + } +} + +async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesModel.Branch): Promise { + const messages = await readAllMessages(dir) + const directory = await client.getDirByName({ + name: dir, + branchId: branch.id, + }) + if (!directory) { + exitWith("Directory not found: " + dir) + } + const files = await client.listFilesByDirectory(directory.id) + console.log(`find ${files.length} files of ${dir}`) + const fileMap = groupBy(files, f => f.name, l => l[0]) + for (const [tsFilename, message] of Object.entries(messages)) { + if (isIgnored(dir, tsFilename)) { + continue + } + console.log(`Start to sync translations of ${tsFilename}`) + const filename = tsFilename.replace('.ts', '.json') + const crowdinFile = fileMap[filename] + if (!crowdinFile) { + console.log(`Failed to find file: dir=${dir}, filename=${tsFilename}`) + continue + } + + for (const locale of ALL_TRANS_LOCALES) { + const strings = transMsg(message[locale]) + const crwodinLang = crowdinLangOf(locale) + await processDirMessage(client, crowdinFile, strings, crwodinLang) + } + } +} + +async function main() { + const client = getClientFromEnv() + const branch = await checkMainBranch(client) + + for (const dir of ALL_DIRS) { + await processDir(client, dir, branch) + } +} + +main() \ No newline at end of file diff --git a/script/user-chart/add.ts b/script/user-chart/add.ts index 2a1d54fdf..6e2611538 100644 --- a/script/user-chart/add.ts +++ b/script/user-chart/add.ts @@ -7,7 +7,8 @@ import { } from "@src/api/gist" import { AddArgv, Argv, Browser } from "./argv" import fs from "fs" -import { descriptionOf, exitWith, filenameOf, getExistGist } from "./common" +import { descriptionOf, filenameOf, getExistGist } from "./common" +import { exitWith } from "../util/process" export type UserCount = Record diff --git a/script/user-chart/argv.ts b/script/user-chart/argv.ts index 122c3033d..7fbaf8aee 100644 --- a/script/user-chart/argv.ts +++ b/script/user-chart/argv.ts @@ -1,4 +1,4 @@ -import { exitWith } from "./common" +import { exitWith } from "../util/process" type Cmd = | 'add' diff --git a/script/user-chart/common.ts b/script/user-chart/common.ts index a81e272b8..ca079da9f 100644 --- a/script/user-chart/common.ts +++ b/script/user-chart/common.ts @@ -1,11 +1,6 @@ import { findTarget, Gist } from "@api/gist" import { Browser } from "./argv" -export function exitWith(msg: string) { - console.error(msg) - process.exit() -} - /** * Calculate the gist description of target browser */ diff --git a/script/user-chart/index.ts b/script/user-chart/index.ts index 8b2ae1b94..1cad7a01d 100644 --- a/script/user-chart/index.ts +++ b/script/user-chart/index.ts @@ -1,6 +1,6 @@ import { add } from "./add" import { parseArgv } from "./argv" -import { exitWith } from "./common" +import { exitWith } from "../util/process" import { render } from "./render" function main() { diff --git a/script/util/process.ts b/script/util/process.ts new file mode 100644 index 000000000..d941ad86f --- /dev/null +++ b/script/util/process.ts @@ -0,0 +1,5 @@ + +export function exitWith(msg: string) { + console.error(msg) + process.exit() +} \ No newline at end of file diff --git a/src/api/crowdin.ts b/src/api/crowdin.ts new file mode 100644 index 000000000..a5299faf4 --- /dev/null +++ b/src/api/crowdin.ts @@ -0,0 +1,36 @@ + +/** + * Copyright (c) 2022-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { AxiosResponse } from "axios" + +import { CROWDIN_PROJECT_ID } from "@util/constant/url" +import axios from "axios" + +/** + * Used to obtain translation status + */ +const PUBLIC_TOKEN = '5e0d53accb6e8c490a1af2914c0963f78082221d7bc1dcb0d56b8e3856a875e432a2e353a948688e' + +export type TranslationStatusInfo = { + /** + * https://developer.crowdin.com/language-codes/ + */ + languageId: string + translationProgress: number +} + +export async function getTranslationStatus(): Promise { + const limit = 500 + const auth = `Bearer ${PUBLIC_TOKEN}` + const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/languages/progress?limit=${limit}` + const response: AxiosResponse = await axios.get(url, { + headers: { "Authorization": auth } + }) + const data: { data: { data: TranslationStatusInfo }[] } = response.data + return data.data.map(i => i.data) +} \ No newline at end of file diff --git a/src/app/components/common/switch-filter-item.ts b/src/app/components/common/switch-filter-item.ts index 64a8e5bbb..97c7344ac 100644 --- a/src/app/components/common/switch-filter-item.ts +++ b/src/app/components/common/switch-filter-item.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { nextTick, Ref } from "vue" import type { RouteLocation } from "vue-router" diff --git a/src/app/components/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 54ca2568a..00a2ca8f3 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -29,7 +29,7 @@ use([ import { t } from "@app/locale" import timerService, { TimerQueryParam } from "@service/timer-service" -import { locale } from "@util/i18n" +import { locale } from "@i18n" import { formatTime, getWeeksAgo, MILL_PER_DAY, MILL_PER_MINUTE } from "@util/time" import { ElLoading } from "element-plus" import { defineComponent, h, onMounted, ref, Ref } from "vue" diff --git a/src/app/components/dashboard/components/indicator/style.sass b/src/app/components/dashboard/components/indicator/style.sass index 2f81c45db..6cdbb4d9e 100644 --- a/src/app/components/dashboard/components/indicator/style.sass +++ b/src/app/components/dashboard/components/indicator/style.sass @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + .indicator-icon-header height: 50px margin-bottom: 10px diff --git a/src/app/components/dashboard/components/week-on-week.ts b/src/app/components/dashboard/components/week-on-week.ts index 72478d458..0618a518c 100644 --- a/src/app/components/dashboard/components/week-on-week.ts +++ b/src/app/components/dashboard/components/week-on-week.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { Ref } from "vue" import type { FillFlagParam, TimerQueryParam } from "@service/timer-service" import type { ECharts, ComposeOption } from "echarts/core" diff --git a/src/app/components/data-manage/clear/filter/date-filter.ts b/src/app/components/data-manage/clear/filter/date-filter.ts index 92cd5b97f..3b0c16946 100644 --- a/src/app/components/data-manage/clear/filter/date-filter.ts +++ b/src/app/components/data-manage/clear/filter/date-filter.ts @@ -9,7 +9,7 @@ import { ElDatePicker } from "element-plus" import { Ref, h } from "vue" import { formatTime, MILL_PER_DAY } from "@util/time" import { t, tN } from "@app/locale" -import { DataManageMessage } from "@app/locale/components/data-manage" +import { DataManageMessage } from "@i18n/message/app/data-manage" import { stepNoClz } from "./constants" export type DateFilterProps = { diff --git a/src/app/components/data-manage/clear/filter/number-filter.ts b/src/app/components/data-manage/clear/filter/number-filter.ts index d15bdcf8c..6d8be5f98 100644 --- a/src/app/components/data-manage/clear/filter/number-filter.ts +++ b/src/app/components/data-manage/clear/filter/number-filter.ts @@ -8,7 +8,7 @@ import { ElInput } from "element-plus" import { Ref, h } from "vue" import { t, tN } from "@app/locale" -import { DataManageMessage } from "@app/locale/components/data-manage" +import { DataManageMessage } from "@i18n/message/app/data-manage" import { stepNoClz } from "./constants" const elInput = (valRef: Ref, placeholder: string, min?: Ref) => 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 717f63c17..16a697df3 100644 --- a/src/app/components/data-manage/clear/filter/operation-button.ts +++ b/src/app/components/data-manage/clear/filter/operation-button.ts @@ -9,9 +9,9 @@ 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 { ItemMessage } from "@util/i18n/components/item" +import { ItemMessage } from "@i18n/message/common/item" import { t } from "@src/app/locale" -import { DataManageMessage } from "@src/app/locale/components/data-manage" +import { DataManageMessage } from "@i18n/message/app/data-manage" import { MILL_PER_DAY } from "@util/time" import { ElementButtonType } from "@src/element-ui/button" diff --git a/src/app/components/data-manage/style/index.sass b/src/app/components/data-manage/style/index.sass index 912979970..c7e3a0325 100644 --- a/src/app/components/data-manage/style/index.sass +++ b/src/app/components/data-manage/style/index.sass @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + .data-manage-container .el-card__body height: 450px diff --git a/src/app/components/habit/component/filter.ts b/src/app/components/habit/component/filter.ts index 00d7943a8..ceb4c11f2 100644 --- a/src/app/components/habit/component/filter.ts +++ b/src/app/components/habit/component/filter.ts @@ -6,7 +6,7 @@ */ import type { PropType } from "vue" -import type { HabitMessage } from "@app/locale/components/habit" +import type { HabitMessage } from "@i18n/message/app/habit" import { ref, Ref, h, defineComponent } from "vue" import { daysAgo } from "@util/time" diff --git a/src/app/components/help-us/alert-info.ts b/src/app/components/help-us/alert-info.ts new file mode 100644 index 000000000..703e37011 --- /dev/null +++ b/src/app/components/help-us/alert-info.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" +import { HelpUsMessage } from "@i18n/message/app/help-us" +import { ElAlert } from "element-plus" +import { defineComponent, h } from "vue" + +const title = t(msg => msg.helpUs.title) + +const liKeys: (keyof HelpUsMessage['alert'])[] = ["l1", "l2", "l3", "l4"] + +const _default = defineComponent({ + name: "HelpUsAlertInfo", + render: () => h(ElAlert, + { type: 'info', title }, + () => liKeys.map(key => h('li', t(msg => msg.helpUs.alert[key]))) + ) +}) + +export default _default diff --git a/src/app/components/help-us/index.ts b/src/app/components/help-us/index.ts new file mode 100644 index 000000000..3f85500de --- /dev/null +++ b/src/app/components/help-us/index.ts @@ -0,0 +1,21 @@ +import { ElCard } from "element-plus" +import { defineComponent, h } from "vue" +import ContentContainer from "../common/content-container" +import HelpUsAlertInfo from "./alert-info" +import HelpUsToolbar from "./toolbar" +import HelpUsProgressList from "./progress-list" +import "./style" + +const _default = defineComponent({ + name: "HelpUs", + render: () => h(ContentContainer, () => h(ElCard, + { class: 'help-us' }, + () => [ + h(HelpUsAlertInfo), + h(HelpUsToolbar), + h(HelpUsProgressList), + ]) + ), +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts new file mode 100644 index 000000000..1e58786d7 --- /dev/null +++ b/src/app/components/help-us/progress-list.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { getTranslationStatus, TranslationStatusInfo } from "@api/crowdin" +import { ElLoading, ElProgress } from "element-plus" +import { defineComponent, h, onMounted, Ref, ref, VNode } from "vue" +import localeMessages from "@i18n/message/common/locale" +import { t } from "@app/locale" + +type SupportedLocale = timer.Locale | timer.TranslatingLocale + +const localeCrowdMap: { [locale in SupportedLocale]: string } = { + en: "en", + zh_CN: "zh-CN", + ja: "ja", + zh_TW: "zh-TW", + de: "de", + en_GB: "en-GB", + en_US: "en-US", + es: "es-ES", + ko: "ko", + pl: "pl", + pt: "pt-PT", + pt_BR: "pt-BR", + ru: "ru" +} + +const crowdLocaleMap: { [locale: string]: SupportedLocale } = {} + +Object.entries(localeCrowdMap).forEach(([locale, crwodLang]) => crowdLocaleMap[crwodLang] = locale as SupportedLocale) + +type ProgressInfo = { + locale: SupportedLocale | string + progress: number +} + +function convert2Info(translationStatus: TranslationStatusInfo): ProgressInfo { + const { languageId, translationProgress } = translationStatus + return { + locale: crowdLocaleMap[languageId] || languageId, + progress: translationProgress + } +} + +function computeType(progress: number): 'success' | '' | 'warning' { + if (progress >= 95) { + return "success" + } else if (progress >= 80) { + return "" + } else { + return "warning" + } +} + +function renderProgressItem(progressInfo: ProgressInfo): VNode { + const { locale, progress } = progressInfo + return h(ElProgress, { percentage: progress, strokeWidth: 22, status: computeType(progress) }, () => [ + h('span', { class: 'progress-text' }, `${progress}%`), + h('span', { class: 'language-name' }, localeMessages[locale] || locale), + ]) +} + +const CONTAINER_CLZ = 'progress-container' + +async function queryData(listRef: Ref) { + const loading = ElLoading.service({ target: `.${CONTAINER_CLZ}`, text: t(msg => msg.helpUs.loading) }) + const langList = await getTranslationStatus() + listRef.value = langList.map(convert2Info).sort((a, b) => b.progress - a.progress) + loading.close() +} + +const _default = defineComponent({ + name: 'HelpUsProgressList', + setup() { + const list: Ref = ref([]) + onMounted(() => queryData(list)) + return () => h('div', { class: CONTAINER_CLZ }, list.value.map(renderProgressItem)) + }, +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/help-us/style.sass b/src/app/components/help-us/style.sass new file mode 100644 index 000000000..1acd46db9 --- /dev/null +++ b/src/app/components/help-us/style.sass @@ -0,0 +1,27 @@ +.help-us + .toolbar-container + margin: 30px 0 + width: 100% + display: flex + .progress-container + display: flex + flex-direction: row + flex-wrap: wrap + width: 100% + min-height: 128px + .el-progress + flex: 0 0 25% + margin-bottom: 30px + width: 400px + font-size: 16px + .el-progress__text + top: 35% + color: var(--el-text-color-regular) + padding-left: 15px + .language-name,.progress-text + display: block + font-size: 12px + .language-name + margin-top: 10px + .el-progress:not(:nth-child(3n)) + margin-right: 12.5% diff --git a/src/app/components/help-us/toolbar.ts b/src/app/components/help-us/toolbar.ts new file mode 100644 index 000000000..b8b565713 --- /dev/null +++ b/src/app/components/help-us/toolbar.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" +import { Pointer } from "@element-plus/icons-vue" +import { CROWDIN_HOMEPAGE } from "@util/constant/url" +import { ElButton } from "element-plus" +import { defineComponent, h } from "vue" + +function openCrowdin() { + window.open(CROWDIN_HOMEPAGE, '_blank') +} + + +const _default = defineComponent({ + name: "HelpUsToolbar", + render: () => h('div', { + class: 'toolbar-container' + }, [ + h(ElButton, { + type: 'primary', + size: 'large', + icon: Pointer, + onClick: openCrowdin + }, () => t(msg => msg.helpUs.button)), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/common.ts b/src/app/components/option/common.ts index f4fbc3344..4d74535ac 100644 --- a/src/app/components/option/common.ts +++ b/src/app/components/option/common.ts @@ -9,7 +9,7 @@ import { InfoFilled } from "@element-plus/icons-vue" import { ElTag, ElTooltip, ElIcon } from "element-plus" import { h, isVNode, VNode } from "vue" import { tN, t, I18nKey } from "@app/locale" -import { OptionMessage } from "@app/locale/components/option" +import { OptionMessage } from "@i18n/message/app/option" /** * Render the option item diff --git a/src/app/components/option/components/appearance/dark-mode-input.ts b/src/app/components/option/components/appearance/dark-mode-input.ts index 1e1be8097..35e8ad236 100644 --- a/src/app/components/option/components/appearance/dark-mode-input.ts +++ b/src/app/components/option/components/appearance/dark-mode-input.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { Ref, PropType, ComputedRef } from "vue" import { ElOption, ElSelect, ElTimePicker } from "element-plus" diff --git a/src/app/components/option/components/appearance/index.ts b/src/app/components/option/components/appearance/index.ts index 8df8e2599..8c2dc4e55 100644 --- a/src/app/components/option/components/appearance/index.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -14,8 +14,8 @@ import { defaultAppearance } from "@util/constant/option" import DarkModeInput from "./dark-mode-input" import { t, tWith } from "@app/locale" import { renderOptionItem, tagText } from "../../common" -import localeMessages from "@util/i18n/components/locale" -import { localeSameAsBrowser } from "@util/i18n" +import localeMessages from "@i18n/message/common/locale" +import { localeSameAsBrowser } from "@i18n" import { toggle } from "@util/dark-mode" const displayWhitelist = (option: UnwrapRef) => h(ElSwitch, { @@ -75,7 +75,7 @@ const locale = (option: UnwrapRef) => h(ElSelect, locale => h(ElOption, { value: locale, label: locale === "default" ? t(msg => msg.option.appearance.locale.default) - : localeMessages[locale].name + : localeMessages[locale] }) ) }) diff --git a/src/app/components/option/components/popup.ts b/src/app/components/option/components/popup.ts index bed7f7311..036e5b7af 100644 --- a/src/app/components/option/components/popup.ts +++ b/src/app/components/option/components/popup.ts @@ -15,7 +15,7 @@ import { renderOptionItem, tagText } from "../common" import { defaultPopup } from "@util/constant/option" import { ALL_POPUP_DURATION } from "@util/constant/popup" import { ALL_DIMENSIONS } from "@util/stat" -import { locale } from "@util/i18n" +import { locale } from "@i18n" import { rotate } from "@util/array" const mergeDomain = (option: UnwrapRef) => h(ElSwitch, { @@ -48,7 +48,7 @@ const typeSelect = (option: UnwrapRef) => h(ElSelect, } }, { default: typeOptions }) -const durationOptions = () => ALL_POPUP_DURATION.map(item => h(ElOption, { value: item, label: t(msg => msg.option.popup.duration[item]) })) +const durationOptions = () => ALL_POPUP_DURATION.map(item => h(ElOption, { value: item, label: t(msg => msg.duration[item]) })) const durationSelect = (option: UnwrapRef) => h(ElSelect, { modelValue: option.defaultDuration, size: 'small', @@ -87,7 +87,7 @@ const weekStartSelect = (option: UnwrapRef) => h(ElSel const defaultPopOptions = defaultPopup() const defaultTypeLabel = t(msg => msg.item[defaultPopOptions.defaultType]) -const defaultDurationLabel = t(msg => msg.option.popup.duration[defaultPopOptions.defaultDuration]) +const defaultDurationLabel = t(msg => msg.duration[defaultPopOptions.defaultDuration]) const displayDefaultLabel = `${defaultDurationLabel}/${defaultTypeLabel}` function copy(target: timer.option.PopupOption, source: timer.option.PopupOption) { diff --git a/src/app/components/report/filter/index.ts b/src/app/components/report/filter/index.ts index 260c7d7f2..4d78d9d34 100644 --- a/src/app/components/report/filter/index.ts +++ b/src/app/components/report/filter/index.ts @@ -4,10 +4,11 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { FileFormat } from "./download-file" import type { Ref, PropType } from "vue" import type { ElementDatePickerShortcut } from "@src/element-ui/date" -import type { ReportMessage } from "@app/locale/components/report" +import type { ReportMessage } from "@i18n/message/app/report" import DownloadFile from "./download-file" import RemoteClient from "./remote-client" diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index 193ca45f0..d6ebc7eaa 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -20,7 +20,7 @@ import { TREND_ROUTE } from "@app/router/constants" import { Open, Plus, Stopwatch } from "@element-plus/icons-vue" import OperationPopupConfirmButton from "@app/components/common/popup-confirm-button" import OperationDeleteButton from "./operation-delete-button" -import { locale } from "@util/i18n" +import { locale } from "@i18n" const timerDatabase = new TimerDatabase(chrome.storage.local) diff --git a/src/app/components/rule-merge/alert-info.ts b/src/app/components/rule-merge/alert-info.ts index 2d30199fa..edbad3b13 100644 --- a/src/app/components/rule-merge/alert-info.ts +++ b/src/app/components/rule-merge/alert-info.ts @@ -8,7 +8,7 @@ import { ElAlert } from "element-plus" import { t, tN } from "@app/locale" import { defineComponent, h } from "vue" -import { MergeRuleMessage } from "@app/locale/components/merge-rule" +import { MergeRuleMessage } from "@i18n/message/app/merge-rule" import { PSL_HOMEPAGE } from "@util/constant/url" const liKeys: (keyof MergeRuleMessage)[] = ['infoAlert0', 'infoAlert1', 'infoAlert2', 'infoAlert3', 'infoAlert4'] @@ -31,15 +31,13 @@ function renderPslLink() { const _default = defineComponent({ name: "RuleMergeAlertInfo", - render() { - return h(ElAlert, - { type: 'info', title }, - () => [ - ...liKeys.map(key => h('li', t(msg => msg.mergeRule[key]))), - h('li', tN(msg => msg.mergeRule.infoAlert5, { psl: renderPslLink() })) - ] - ) - } + render: () => h(ElAlert, + { type: 'info', title }, + () => [ + ...liKeys.map(key => h('li', t(msg => msg.mergeRule[key]))), + h('li', tN(msg => msg.mergeRule.infoAlert5, { psl: renderPslLink() })) + ] + ) }) export default _default \ No newline at end of file diff --git a/src/app/components/rule-merge/index.ts b/src/app/components/rule-merge/index.ts index 03e694a83..9ee3ab707 100644 --- a/src/app/components/rule-merge/index.ts +++ b/src/app/components/rule-merge/index.ts @@ -15,12 +15,10 @@ import { h } from "vue" const _default = defineComponent({ name: "RuleMerge", - setup() { - return () => h(ContentContainer, () => h(ElCard, () => [ - h(RuleMergeAlertInfo), - itemList() - ])) - } + render: () => h(ContentContainer, () => h(ElCard, () => [ + h(RuleMergeAlertInfo), + itemList() + ])) }) export default _default diff --git a/src/app/components/trend/components/filter.ts b/src/app/components/trend/components/filter.ts index 89bc0992d..a006336d3 100644 --- a/src/app/components/trend/components/filter.ts +++ b/src/app/components/trend/components/filter.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { Ref, PropType } from "vue" import { ElOption, ElSelect } from "element-plus" @@ -11,7 +12,7 @@ import { ref, h, defineComponent } from "vue" import timerService, { HostSet } from "@service/timer-service" import { daysAgo } from "@util/time" import { t } from "@app/locale" -import { TrendMessage } from "@app/locale/components/trend" +import { TrendMessage } from "@i18n/message/app/trend" import DateRangeFilterItem from "@app/components/common/date-range-filter-item" import SelectFilterItem from "@app/components/common/select-filter-item" import { ElementDatePickerShortcut } from "@src/element-ui/date" diff --git a/src/app/components/whitelist/alert-info.ts b/src/app/components/whitelist/alert-info.ts index 13c2fc58c..09c4d2a2a 100644 --- a/src/app/components/whitelist/alert-info.ts +++ b/src/app/components/whitelist/alert-info.ts @@ -14,7 +14,7 @@ import { h } from "vue" import { ElAlert } from "element-plus" import { t } from "@app/locale" -import { WhitelistMessage } from "@app/locale/components/whitelist" +import { WhitelistMessage } from "@i18n/message/app/whitelist" const title = t(msg => msg.whitelist.infoAlertTitle) diff --git a/src/app/components/whitelist/index.ts b/src/app/components/whitelist/index.ts index 11707e9ce..0e834c049 100644 --- a/src/app/components/whitelist/index.ts +++ b/src/app/components/whitelist/index.ts @@ -13,15 +13,11 @@ import itemList from "./item-list" import { ElCard } from "element-plus" import { h } from "vue" -const _default = defineComponent( - { - name: "Whitelist", - setup() { - return () => h(ContentContainer, {}, { - default: () => h(ElCard, {}, () => [alertInfo(), itemList()]) - }) - } - } -) +const _default = defineComponent({ + name: "Whitelist", + render: () => h(ContentContainer, + () => h(ElCard, () => [alertInfo(), itemList()]) + ) +}) export default _default diff --git a/src/app/index.ts b/src/app/index.ts index 3103389a7..55b049925 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -15,7 +15,7 @@ import './styles' // global css import installRouter from "./router" import '../common/timer' import ElementPlus from 'element-plus' -import { initLocale, locale as appLocale } from "@util/i18n" +import { initLocale, locale as appLocale } from "@i18n" import { toggle, init as initTheme } from "@util/dark-mode" import optionService from "@service/option-service" import "@src/common/timer" diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index f7529a24b..994c52013 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -4,19 +4,20 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { UnwrapRef } from "vue" -import type ElementIcon from "../../element-ui/icon" +import type ElementIcon from "@src/element-ui/icon" import type { RouteLocationNormalizedLoaded, Router } from "vue-router" import type { I18nKey } from "@app/locale" -import type { MenuMessage } from "@app/locale/components/menu" +import type { MenuMessage } from "@i18n/message/app/menu" import { defineComponent, h, onMounted, reactive } from "vue" import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" -import { HOME_PAGE, TRANSLATION_ISSUE_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" -import { Aim, Calendar, ChatSquare, Folder, HotWater, MagicStick, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" -import { locale } from "@util/i18n" +import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" +import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" +import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" type _MenuItem = { @@ -41,14 +42,12 @@ type _RouteProps = { * Generate menu items after locale initialized */ function generateMenus(): _MenuGroup[] { - /** - * Use TU_CAO_PAGE, if the locale is Chinese - * - * @since 0.9.0 - */ - const isZhCn = locale === "zh_CN" - - const otherMenuItems: _MenuItem[] = [] + const otherMenuItems: _MenuItem[] = [{ + title: 'helpUs', + route: '/other/help', + icon: HelpFilled, + index: '_i18n' + }] HOME_PAGE && otherMenuItems.push({ title: 'rate', href: HOME_PAGE, @@ -62,14 +61,6 @@ function generateMenus(): _MenuGroup[] { icon: ChatSquare, index: '_feedback' }) - if (!isZhCn) { - otherMenuItems.push({ - title: 'translationMistake', - href: TRANSLATION_ISSUE_PAGE, - icon: MagicStick, - index: '_i18n' - }) - } // All menu items return [{ diff --git a/src/app/locale/index.ts b/src/app/locale.ts similarity index 57% rename from src/app/locale/index.ts rename to src/app/locale.ts index 60b3b1d03..79c93f7e2 100644 --- a/src/app/locale/index.ts +++ b/src/app/locale.ts @@ -5,15 +5,15 @@ * https://opensource.org/licenses/MIT */ -import { I18nKey as _I18nKey, locale, t as _t } from "@util/i18n" -import { tN as _tN } from "@util/i18n/i18n-vue" -import messages, { AppMessage } from "./messages" +import { I18nKey as _I18nKey, t as _t } from "@i18n" +import { tN as _tN } from "@i18n/i18n-vue" +import messages, { AppMessage } from "@i18n/message/app" export type I18nKey = _I18nKey export function t(key: I18nKey, param?: any) { const props = { key, param } - return _t(messages[locale], props) + return _t(messages, props) } /** @@ -21,9 +21,9 @@ export function t(key: I18nKey, param?: any) { */ export function tWith(key: I18nKey, specLocale: timer.Locale, param?: any) { const props = { key, param } - return _t(messages[specLocale], props) + return _t(messages, props, specLocale) } export function tN(key: I18nKey, param?: any) { - return _tN(messages[locale], { key, param }) + return _tN(messages, { key, param }) } \ No newline at end of file diff --git a/src/app/locale/components/site-manage.ts b/src/app/locale/components/site-manage.ts deleted file mode 100644 index 12007b61f..000000000 --- a/src/app/locale/components/site-manage.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { Messages } from "@util/i18n" - -export type SiteManageMessage = { - hostPlaceholder: string - aliasPlaceholder: string - onlyDetected: string - deleteConfirmMsg: string - column: { - host: string - alias: string - aliasInfo: string - source: string - } - source: { - user: string - detected: string - } - button: { - add: string - delete: string - modify: string - save: string - } - form: { - emptyAlias: string - emptyHost: string - } - msg: { - hostExistWarn: string - saved: string - existedTag: string - mergedTag: string - } -} - -const en: SiteManageMessage = { - hostPlaceholder: "Partial URL, then enter", - aliasPlaceholder: "Partial name, then enter", - onlyDetected: 'Only detected', - deleteConfirmMsg: 'The name of {host} will be deleted', - column: { - host: "Site URL", - alias: "Site Name", - aliasInfo: "The site name will be shown on the record page and the popup page", - source: "Source" - }, - source: { - user: 'user-maintained', - detected: 'auto-detected' - }, - button: { - add: 'New', - delete: 'Delete', - modify: 'Modify', - save: 'Save', - }, - form: { - emptyAlias: 'Please enter site name', - emptyHost: 'Please enter site URL' - }, - msg: { - hostExistWarn: '{host} exists', - saved: 'Saved', - existedTag: 'EXISTED', - mergedTag: 'MERGED' - } -} - -const ja: SiteManageMessage = { - hostPlaceholder: "ドメイン名で検索", - aliasPlaceholder: "サイト名で検索", - onlyDetected: '検出されただけ', - deleteConfirmMsg: '{host} の名前が削除されます', - column: { - host: "サイトのURL", - alias: "サイト名", - aliasInfo: "サイト名はレコードページとポップアップページに表示されます", - source: "ソース" - }, - source: { - user: '手动输入', - detected: 'システム検出' - }, - button: { - add: '追加', - delete: '削除', - modify: '変更', - save: '保存', - }, - form: { - emptyAlias: 'サイト名を入力してください', - emptyHost: 'ドメイン名を入力してください' - }, - msg: { - hostExistWarn: '{host} が存在します', - saved: '保存しました', - existedTag: '既存', - mergedTag: '合并' - } -} - -const messages: Messages = { - zh_CN: { - hostPlaceholder: "请输入域名,然后回车", - aliasPlaceholder: "请输入网站名,然后回车", - onlyDetected: '只看自动抓取', - deleteConfirmMsg: '{host} 的名称设置将会被删除', - column: { - host: "网站域名", - alias: "网站名称", - aliasInfo: "网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名", - source: "来源" - }, - source: { - user: '手动设置', - detected: '自动抓取' - }, - button: { - add: '新增', - delete: '删除', - modify: '修改', - save: '保存' - }, - form: { - emptyAlias: '请输入网站名称', - emptyHost: '请输入网站域名' - }, - msg: { - hostExistWarn: '{host} 已经存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合并' - } - }, - zh_TW: { - hostPlaceholder: "請輸入網域,然後回車", - aliasPlaceholder: "請輸入網站名,然後回車", - onlyDetected: '隻看自動抓取', - deleteConfirmMsg: '{host} 的名稱設置將會被刪除', - column: { - host: "網站域名", - alias: "網站名稱", - aliasInfo: "網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域", - source: "來源" - }, - source: { - user: '手動設置', - detected: '自動抓取' - }, - button: { - add: '新增', - delete: '刪除', - modify: '修改', - save: '保存' - }, - form: { - emptyAlias: '請輸入網站名稱', - emptyHost: '請輸入網站域名' - }, - msg: { - hostExistWarn: '{host} 已經存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合並' - } - }, - en, - // Feedback - ja -} - -export default messages diff --git a/src/app/router/index.ts b/src/app/router/index.ts index b28636ca7..dace0236a 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -66,7 +66,16 @@ const additionalRoutes: RouteRecordRaw[] = [ } ] -const otherRoutes: RouteRecordRaw[] = [] +const otherRoutes: RouteRecordRaw[] = [ + { + path: '/other', + redirect: '/other/help' + }, + { + path: '/other/help', + component: () => import('../components/help-us'), + } +] const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/data' }, diff --git a/src/background/backup/gist/coordinator.ts b/src/background/backup/gist/coordinator.ts index 6858a3333..2cbbe128e 100644 --- a/src/background/backup/gist/coordinator.ts +++ b/src/background/backup/gist/coordinator.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import type { Gist, GistForm, File, FileForm } from "@api/gist" import { getJsonFileContent, findTarget, getGist, createGist, updateGist, testToken } from "@api/gist" diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 2f609ae25..0d4fb41bc 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -7,7 +7,7 @@ import { OPTION_ROUTE } from "../app/router/constants" import { getAppPageUrl, getGuidePageUrl, SOURCE_CODE_PAGE, TU_CAO_PAGE } from "@util/constant/url" -import { t2Chrome } from "@util/i18n/chrome/t" +import { t2Chrome } from "@i18n/chrome/t" import { IS_SAFARI } from "@util/constant/environment" const APP_PAGE_URL = getAppPageUrl(true) @@ -31,7 +31,7 @@ function titleOf(prefixEmoji: string, title: string) { const allFunctionProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_app_link', - title: titleOf('🏷️', t2Chrome(msg => msg.contextMenus.allFunctions)), + title: titleOf('🏷️', t2Chrome(msg => msg.base.allFunction)), onclick: () => chrome.tabs.create({ url: APP_PAGE_URL }), ...baseProps } @@ -59,7 +59,7 @@ const feedbackPageProps: chrome.contextMenus.CreateProperties = { const guidePageProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_guide_link', - title: titleOf('📖', t2Chrome(msg => msg.contextMenus.guidePage)), + title: titleOf('📖', t2Chrome(msg => msg.base.guidePage)), onclick: () => chrome.tabs.create({ url: getGuidePageUrl(true) }), ...baseProps } diff --git a/src/background/uninstall-listener.ts b/src/background/uninstall-listener.ts index e49891aef..f9ef479cd 100644 --- a/src/background/uninstall-listener.ts +++ b/src/background/uninstall-listener.ts @@ -6,7 +6,7 @@ */ import { UNINSTALL_QUESTIONNAIRE } from "@util/constant/url" -import { locale } from "@util/i18n" +import { locale } from "@i18n" async function listen() { try { 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 9189c9f65..021c4d7e7 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 @@ -9,7 +9,7 @@ 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 IVersionProcessor from "../i-version-processor" -import { t2Chrome } from "@util/i18n/chrome/t" +import { t2Chrome } from "@i18n/chrome/t" const storage: chrome.storage.StorageArea = chrome.storage.local diff --git a/src/background/whitelist-menu-manager.ts b/src/background/whitelist-menu-manager.ts index c5d5e381c..bfe18e6b5 100644 --- a/src/background/whitelist-menu-manager.ts +++ b/src/background/whitelist-menu-manager.ts @@ -7,8 +7,8 @@ import WhitelistDatabase from "@db/whitelist-database" import optionService from "@service/option-service" -import { t2Chrome } from "@util/i18n/chrome/t" -import { ContextMenusMessage } from "@util/i18n/components/context-menus" +import { t2Chrome } from "@i18n/chrome/t" +import { ContextMenusMessage } from "@i18n/message/common/context-menus" import { extractHostname, isBrowserUrl } from "@util/pattern" const db = new WhitelistDatabase(chrome.storage.local) diff --git a/src/content-script/index.ts b/src/content-script/index.ts index e8dc573d3..552fa5f25 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { initLocale } from "@util/i18n" +import { initLocale } from "@i18n" import processLimit from "./limit" import printInfo from "./printer" diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index c613ca77d..baa8190e6 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -7,7 +7,7 @@ import TimeLimitItem from "@entity/time-limit-item" import optionService from "@service/option-service" -import { t2Chrome } from "@util/i18n/chrome/t" +import { t2Chrome } from "@i18n/chrome/t" function moreMinutes(url: string): Promise { const request: timer.mq.Request = { @@ -148,7 +148,7 @@ function link2Setup(url: string): HTMLParagraphElement { Object.assign(link.style, linkStyle) link.setAttribute('href', 'javascript:void(0)') const text = t2Chrome(msg => msg.message.timeLimitMsg) - .replace('{appName}', t2Chrome(msg => msg.app.name)) + .replace('{appName}', t2Chrome(msg => msg.meta.name)) link.innerText = text link.onclick = () => chrome.runtime.sendMessage(openLimitPageMessage(url)) const p = document.createElement('p') diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 95a98822e..8e1f9c1a1 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { t2Chrome } from "@util/i18n/chrome/t" +import { t2Chrome } from "@i18n/chrome/t" import { formatPeriod } from "@util/time" function getTodayInfo(host: string): Promise { diff --git a/src/echarts/chart/bar.ts b/src/echarts/chart/bar.ts index f51d2b9ba..a2ac54b51 100644 --- a/src/echarts/chart/bar.ts +++ b/src/echarts/chart/bar.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as BarChart } from "echarts/lib/chart/bar/install" export default BarChart \ No newline at end of file diff --git a/src/echarts/chart/candlestick.ts b/src/echarts/chart/candlestick.ts index 44fb0a562..2e51090a5 100644 --- a/src/echarts/chart/candlestick.ts +++ b/src/echarts/chart/candlestick.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as CandlestickChart } from "echarts/lib/chart/candlestick/install" export default CandlestickChart \ No newline at end of file diff --git a/src/echarts/chart/heatmap.ts b/src/echarts/chart/heatmap.ts index f5fbb1cc3..c0e72c8b7 100644 --- a/src/echarts/chart/heatmap.ts +++ b/src/echarts/chart/heatmap.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as HeatmapChart } from "echarts/lib/chart/heatmap/install" export default HeatmapChart \ No newline at end of file diff --git a/src/echarts/chart/line.ts b/src/echarts/chart/line.ts index 06b558a32..c64313312 100644 --- a/src/echarts/chart/line.ts +++ b/src/echarts/chart/line.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as LineChart } from "echarts/lib/chart/line/install" export default LineChart \ No newline at end of file diff --git a/src/echarts/chart/pie.ts b/src/echarts/chart/pie.ts index a263eb22a..b5d9c99fa 100644 --- a/src/echarts/chart/pie.ts +++ b/src/echarts/chart/pie.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as PieChart } from "echarts/lib/chart/pie/install" export default PieChart \ No newline at end of file diff --git a/src/echarts/component/grid.ts b/src/echarts/component/grid.ts index 8a03b4b52..ea787c7ee 100644 --- a/src/echarts/component/grid.ts +++ b/src/echarts/component/grid.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as GridComponent } from "echarts/lib/component/grid/install" export default GridComponent \ No newline at end of file diff --git a/src/echarts/component/legend.ts b/src/echarts/component/legend.ts index 377a0292a..0ced9a204 100644 --- a/src/echarts/component/legend.ts +++ b/src/echarts/component/legend.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as LegendComponent } from "echarts/lib/component/legend/install" export default LegendComponent \ No newline at end of file diff --git a/src/echarts/component/title.ts b/src/echarts/component/title.ts index 6ea7d5ea6..4dd546945 100644 --- a/src/echarts/component/title.ts +++ b/src/echarts/component/title.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as TitleComponent } from "echarts/lib/component/title/install" export default TitleComponent \ No newline at end of file diff --git a/src/echarts/component/toolbox.ts b/src/echarts/component/toolbox.ts index 63e900073..110809071 100644 --- a/src/echarts/component/toolbox.ts +++ b/src/echarts/component/toolbox.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as ToolboxComponent } from "echarts/lib/component/toolbox/install" export default ToolboxComponent \ No newline at end of file diff --git a/src/echarts/component/tooltip.ts b/src/echarts/component/tooltip.ts index c4446d5de..b2c7e0858 100644 --- a/src/echarts/component/tooltip.ts +++ b/src/echarts/component/tooltip.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as TooltipComponent } from "echarts/lib/component/tooltip/install" export default TooltipComponent \ No newline at end of file diff --git a/src/echarts/component/visual-map.ts b/src/echarts/component/visual-map.ts index 993c1c712..c4ce521d8 100644 --- a/src/echarts/component/visual-map.ts +++ b/src/echarts/component/visual-map.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { install as VisualMapComponent } from "echarts/lib/component/visualMap/install" export default VisualMapComponent \ No newline at end of file diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts index 52c3461a2..923831a61 100644 --- a/src/guide/component/common.ts +++ b/src/guide/component/common.ts @@ -4,8 +4,8 @@ import type { VNode } from "vue" import { t, tN } from "@guide/locale" import { h } from "vue" -export function h1(i18nKey: I18nKey, archorClz: string): VNode { - return h('h1', { class: `guide-h1 archor-${archorClz}` }, t(i18nKey)) +export function h1(i18nKey: I18nKey, archorClz: string, i18nParam?: any): VNode { + return h('h1', { class: `guide-h1 archor-${archorClz}` }, t(i18nKey, i18nParam)) } export function h2(i18nKey: I18nKey, archorClz: string): VNode { diff --git a/src/guide/component/profile.ts b/src/guide/component/profile.ts index 0b844dfd6..9f1b6651e 100644 --- a/src/guide/component/profile.ts +++ b/src/guide/component/profile.ts @@ -6,6 +6,7 @@ * https://opensource.org/licenses/MIT */ +import { t } from "@guide/locale" import { EDGE_HOMEPAGE, CHROME_HOMEPAGE, FIREFOX_HOMEPAGE, SOURCE_CODE_PAGE } from "@util/constant/url" import { defineComponent } from "vue" @@ -15,12 +16,13 @@ const _default = defineComponent({ name: 'GuideProfile', setup() { return () => section( - h1(msg => msg.layout.menu.profile, 'profile'), + h1(msg => msg.layout.menu.profile, 'profile', { appName: t(msg => msg.meta.name) }), paragraph(msg => msg.profile.p1, { edge: link(EDGE_HOMEPAGE, 'Edge'), chrome: link(CHROME_HOMEPAGE, 'Chrome'), firefox: link(FIREFOX_HOMEPAGE, 'Firefox'), github: link(SOURCE_CODE_PAGE, 'Github'), + appName: t(msg => msg.meta.name), }), paragraph(msg => msg.profile.p2), ) diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts index 9e886d1ca..4308dbfad 100644 --- a/src/guide/component/usage.ts +++ b/src/guide/component/usage.ts @@ -22,8 +22,8 @@ const background = () => [ background: linkInner(backgroundPageUrl, t(msg => msg.usage.background.backgroundPage)) }), list( - msg => msg.usage.background.l1, - msg => msg.usage.background.l2, + [msg => msg.usage.background.l1, { allFunction: t(msg => msg.base.allFunction) }], + [msg => msg.usage.background.l2, { allFunction: t(msg => msg.base.allFunction) }], ), paragraph(msg => msg.usage.background.p2), ] diff --git a/src/guide/index.ts b/src/guide/index.ts index 0d570fc42..b87af4365 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -4,10 +4,11 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import "./style" import 'element-plus/theme-chalk/index.css' -import { initLocale } from "@util/i18n" +import { initLocale } from "@i18n" import { t } from "./locale" import { init as initTheme } from "@util/dark-mode" import { createApp } from "vue" @@ -21,7 +22,7 @@ async function main() { const app = createApp(Main) app.mount('#guide') - document.title = t(msg => msg.layout.title) + document.title = t(msg => msg.base.guidePage) + ' | ' + t(msg => msg.meta.name) } main() diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index c7e9ece5d..025e816b4 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -99,7 +99,7 @@ const _default = defineComponent({ onClick: () => handleClick(profilePosition) }, () => [ h(ElIcon, () => h(MagicStick)), - h('span', {}, t(msg => msg.layout.menu.profile)) + h('span', {}, t(msg => msg.layout.menu.profile, { appName: t(msg => msg.meta.name) })) ]), ...menus.map( group => renderGroup(handleClick, group) diff --git a/src/guide/locale/index.ts b/src/guide/locale.ts similarity index 56% rename from src/guide/locale/index.ts rename to src/guide/locale.ts index 2a398a309..9d94f23cc 100644 --- a/src/guide/locale/index.ts +++ b/src/guide/locale.ts @@ -5,18 +5,18 @@ * https://opensource.org/licenses/MIT */ -import { I18nKey as _I18nKey, locale, t as _t } from "@util/i18n" -import { tN as _tN } from "@util/i18n/i18n-vue" -import messages, { GuideMessage } from "./messages" +import { I18nKey as _I18nKey, t as _t } from "@i18n" +import { tN as _tN } from "@i18n/i18n-vue" +import messages, { GuideMessage } from "@i18n/message/guide" export type I18nKey = _I18nKey export function t(key: I18nKey, param?: any) { const props = { key, param } - return _t(messages[locale], props) + return _t(messages, props) } export function tN(key: I18nKey, param?: any) { const props = { key, param } - return _tN(messages[locale], props) + return _tN(messages, props) } \ No newline at end of file diff --git a/src/guide/locale/components/profile.ts b/src/guide/locale/components/profile.ts deleted file mode 100644 index c664850f2..000000000 --- a/src/guide/locale/components/profile.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { Messages } from "@util/i18n" -import appMessages from "@util/i18n/components/app" - -type _key = - | 'p1' - | 'p2' - -export type ProfileMessage = { - [key in _key]: string -} - -const _default: Messages = { - zh_CN: { - p1: appMessages.zh_CN.name + '是一款开源、免费、用户友好的,用于统计上网时间的浏览器扩展。您可以在 {github} 上查阅它的源代码。', - p2: '这个页面将会告诉您如何使用它,以及相关的隐私政策。' - }, - zh_TW: { - p1: appMessages.zh_TW.name + '是一款開源、免費、用戶友好的,用於統計上網時間的瀏覽器擴展。您可以在 {github} 上查閱它的源代碼。', - p2: '這個頁面將會告訴您如何使用它,以及相關的隱私政策。' - }, - en: { - p1: appMessages.en.name + ' 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: { - p1: appMessages.ja.name + 'は、オンラインで費やした時間をカウントするための、オープン ソースで無料のユーザー フレンドリーなブラウザ拡張機能です。 {github} でソース コードを確認できます。 ', - p2: 'このページでは、使用方法と関連するプライバシー ポリシーについて説明します。', - } -} - -export default _default \ No newline at end of file diff --git a/src/util/i18n/chrome/compile.ts b/src/i18n/chrome/compile.ts similarity index 100% rename from src/util/i18n/chrome/compile.ts rename to src/i18n/chrome/compile.ts diff --git a/src/util/i18n/chrome/index.ts b/src/i18n/chrome/index.ts similarity index 100% rename from src/util/i18n/chrome/index.ts rename to src/i18n/chrome/index.ts diff --git a/src/util/i18n/chrome/message.ts b/src/i18n/chrome/message.ts similarity index 76% rename from src/util/i18n/chrome/message.ts rename to src/i18n/chrome/message.ts index 998bf260a..c04c2dca8 100644 --- a/src/util/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -5,15 +5,15 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - -import appMessages, { AppMessage } from "../components/app" -import contentScriptMessages, { ContentScriptMessage } from "../components/content-script" -import contextMenusMessages, { ContextMenusMessage } from "../components/context-menus" -import initialMessages, { InitialMessage } from "../components/initial" +import metaMessages, { MetaMessage } from "../message/common/meta" +import contentScriptMessages, { ContentScriptMessage } from "../message/common/content-script" +import contextMenusMessages, { ContextMenusMessage } from "../message/common/context-menus" +import initialMessages, { InitialMessage } from "../message/common/initial" +import baseMessages, { BaseMessage } from "@i18n/message/common/base" export type ChromeMessage = { - app: AppMessage + meta: MetaMessage + base: BaseMessage message: ContentScriptMessage contextMenus: ContextMenusMessage initial: InitialMessage @@ -21,25 +21,29 @@ export type ChromeMessage = { const messages: Messages = { zh_CN: { - app: appMessages.zh_CN, + meta: metaMessages.zh_CN, + base: baseMessages.zh_CN, message: contentScriptMessages.zh_CN, contextMenus: contextMenusMessages.zh_CN, initial: initialMessages.zh_CN, }, zh_TW: { - app: appMessages.zh_TW, + meta: metaMessages.zh_TW, + base: baseMessages.zh_TW, message: contentScriptMessages.zh_TW, contextMenus: contextMenusMessages.zh_TW, initial: initialMessages.zh_TW, }, en: { - app: appMessages.en, + meta: metaMessages.en, + base: baseMessages.en, message: contentScriptMessages.en, contextMenus: contextMenusMessages.en, initial: initialMessages.en, }, ja: { - app: appMessages.ja, + meta: metaMessages.ja, + base: baseMessages.ja, message: contentScriptMessages.ja, contextMenus: contextMenusMessages.ja, initial: initialMessages.ja, @@ -49,10 +53,12 @@ const messages: Messages = { export default messages const placeholder: ChromeMessage = { - app: { + meta: { name: '', description: '', marketName: '', + }, + base: { currentVersion: '', allFunction: '', guidePage: '', @@ -70,11 +76,9 @@ const placeholder: ChromeMessage = { contextMenus: { add2Whitelist: '', removeFromWhitelist: '', - allFunctions: '', optionPage: '', repoPage: '', feedbackPage: '', - guidePage: '', }, initial: { localFile: { diff --git a/src/util/i18n/chrome/t.ts b/src/i18n/chrome/t.ts similarity index 100% rename from src/util/i18n/chrome/t.ts rename to src/i18n/chrome/t.ts diff --git a/src/util/i18n/i18n-vue.ts b/src/i18n/i18n-vue.ts similarity index 91% rename from src/util/i18n/i18n-vue.ts rename to src/i18n/i18n-vue.ts index ad2f1b9b6..9b4175e78 100644 --- a/src/util/i18n/i18n-vue.ts +++ b/src/i18n/i18n-vue.ts @@ -35,7 +35,7 @@ export type NodeTranslateProps = { * Translate with slots for vue * I18nResultItemray of vnodes or strings */ -export function tN(messages: MessageType, props: NodeTranslateProps): I18nResultItem[] { +export function tN(messages: Messages, props: NodeTranslateProps): I18nResultItem[] { const { key, param } = props const result = getI18nVal(messages, key) let resultArr: I18nResultItem[] = [result] diff --git a/src/i18n/i18n.d.ts b/src/i18n/i18n.d.ts new file mode 100644 index 000000000..ffa025784 --- /dev/null +++ b/src/i18n/i18n.d.ts @@ -0,0 +1,4 @@ + +type Messages = { + [locale in timer.Locale]: M +} diff --git a/src/util/i18n/index.ts b/src/i18n/index.ts similarity index 83% rename from src/util/i18n/index.ts rename to src/i18n/index.ts index b773c5478..54a6a3b19 100644 --- a/src/util/i18n/index.ts +++ b/src/i18n/index.ts @@ -18,9 +18,9 @@ const FEEDBACK_LOCALE: timer.Locale = "en" export const defaultLocale: timer.Locale = "zh_CN" -export type Messages = { - [key in timer.Locale]: T -} +// export type Messages = { +// [key in timer.Locale]: T +// } // Standardize the locale code according to the Chrome locale code const chrome2I18n: { [key: string]: timer.Locale } = { @@ -76,8 +76,14 @@ export async function initLocale() { optionService.addOptionChangeListener(handleLocaleOption) -export function getI18nVal(messages: MessageType, keyPath: I18nKey): string { - const result = keyPath(messages) +export function getI18nVal( + messages: Messages, + keyPath: I18nKey, + specLocale?: timer.Locale +): string { + const result = keyPath(messages[specLocale || locale]) + || keyPath(messages[FEEDBACK_LOCALE]) + || '' return typeof result === 'string' ? result : JSON.stringify(result) } @@ -96,9 +102,9 @@ function fillWithParam(result: string, param: { [key: string]: string | number } return result } -export function t(messages: MessageType, props: TranslateProps): string { +export function t(messages: Messages, props: TranslateProps, specLocale?: timer.Locale): string { const { key, param } = props - let result: string = getI18nVal(messages, key) + const result: string = getI18nVal(messages, key, specLocale) return param ? fillWithParam(result, param) : result } diff --git a/src/app/locale/components/confirm.ts b/src/i18n/message/app/confirm.ts similarity index 89% rename from src/app/locale/components/confirm.ts rename to src/i18n/message/app/confirm.ts index 65d6fc336..956e81f86 100644 --- a/src/app/locale/components/confirm.ts +++ b/src/i18n/message/app/confirm.ts @@ -5,11 +5,8 @@ * https://opensource.org/licenses/MIT */ - -import { Messages } from "@util/i18n" - export type ConfirmMessage = { - confirmMsg: string + confirmMsg: string, cancelMsg: string } @@ -29,7 +26,7 @@ const _default: Messages = { ja: { confirmMsg: 'OK', cancelMsg: 'キャンセル', - } + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/dashboard.ts b/src/i18n/message/app/dashboard.ts similarity index 98% rename from src/app/locale/components/dashboard.ts rename to src/i18n/message/app/dashboard.ts index 46fda78ff..6c47609f1 100644 --- a/src/app/locale/components/dashboard.ts +++ b/src/i18n/message/app/dashboard.ts @@ -5,15 +5,13 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type DashboardMessage = { heatMap: { title0: string title1: string tooltip0: string tooltip1: string - } + }, topK: { title: string tooltip: string @@ -128,7 +126,7 @@ const _default: Messages = { installedDays: '使用 {number} 日', visitCount: '{site} つのサイトへの合計 {visit} 回の拜訪', browsingTime: '{minute} 分以上ウェブを閲覧する', - mostUse: '{start}:00 から {end}:00 までのお気に入りのインターネットアクセス' + mostUse: '{start}:00 から {end}:00 までのお気に入りのインターネットアクセス', }, weekOnWeek: { title: '週ごとの変更 TOP {k}', diff --git a/src/app/locale/components/data-manage.ts b/src/i18n/message/app/data-manage.ts similarity index 91% rename from src/app/locale/components/data-manage.ts rename to src/i18n/message/app/data-manage.ts index 48d6eee98..c7f36eb81 100644 --- a/src/app/locale/components/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -1,12 +1,10 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type DataManageMessage = { totalMemoryAlert: string totalMemoryAlert1: string @@ -18,11 +16,6 @@ export type DataManageMessage = { filterTime: string filterDate: string unlimited: string - dateShortcut: { - tillYesterday: string - till7DaysAgo: string - till30DaysAgo: string - } paramError: string deleteConfirm: string deleteSuccess: string @@ -31,6 +24,11 @@ export type DataManageMessage = { migrated: string operationCancel: string operationConfirm: string + dateShortcut: { + tillYesterday: string + till7DaysAgo: string + till30DaysAgo: string + } } const _default: Messages = { @@ -48,7 +46,7 @@ const _default: Messages = { dateShortcut: { tillYesterday: '直到昨天', till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前' + till30DaysAgo: '直到30天前', }, paramError: '参数错误,请检查!', deleteConfirm: '共筛选出 {count} 条数据,是否全部删除?', @@ -56,8 +54,8 @@ const _default: Messages = { migrationAlert: '使用导入/导出在不同浏览器之间迁移数据', importError: '文件格式错误', migrated: '成功导入', - operationCancel: "取消", - operationConfirm: "确认", + operationCancel: '取消', + operationConfirm: '确认', }, zh_TW: { totalMemoryAlert: '瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據', @@ -73,7 +71,7 @@ const _default: Messages = { dateShortcut: { tillYesterday: '直到昨天', till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前' + till30DaysAgo: '直到30天前', }, paramError: '參數錯誤,請檢查!', deleteConfirm: '共篩選出 {count} 條數據,是否全部刪除?', @@ -81,8 +79,8 @@ const _default: Messages = { migrationAlert: '使用導入/導出在不同瀏覽器之間遷移數據', importError: '文件格式錯誤', migrated: '成功導入', - operationCancel: "取消", - operationConfirm: "確認", + operationCancel: '取消', + operationConfirm: '確認', }, en: { totalMemoryAlert: 'The browser provides {size}MB to store local data for each extension', @@ -98,7 +96,7 @@ const _default: Messages = { dateShortcut: { tillYesterday: 'Until yesterday', till7DaysAgo: 'Until 7 days ago', - till30DaysAgo: 'Until 30 days ago' + till30DaysAgo: 'Until 30 days ago', }, paramError: 'The parameter is wrong, please check!', deleteConfirm: 'A total of {count} records have been filtered out. Do you want to delete them all?', @@ -106,8 +104,8 @@ const _default: Messages = { migrationAlert: 'Migrate data between browsers using import and export', importError: 'Wrong file extension', migrated: 'Imported successfully!', - operationCancel: "Cancel", - operationConfirm: "Confirm", + operationCancel: 'Cancel', + operationConfirm: 'Confirm', }, ja: { totalMemoryAlert: 'ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します', @@ -123,7 +121,7 @@ const _default: Messages = { dateShortcut: { tillYesterday: '昨日まで', till7DaysAgo: '7日前まで', - till30DaysAgo: '30日前まで' + till30DaysAgo: '30日前まで', }, paramError: 'パラメータエラー、確認してください!', deleteConfirm: '合計 {count} 個のデータが除外されました。すべて削除しますか?', @@ -131,9 +129,9 @@ const _default: Messages = { migrationAlert: 'インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します', importError: 'ファイル形式エラー', migrated: '正常にインポートされました', - operationCancel: "取消", - operationConfirm: "確認", - } + operationCancel: '取消', + operationConfirm: '確認', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/habit.ts b/src/i18n/message/app/habit.ts similarity index 80% rename from src/app/locale/components/habit.ts rename to src/i18n/message/app/habit.ts index c64ad9fcd..45835e60d 100644 --- a/src/app/locale/components/habit.ts +++ b/src/i18n/message/app/habit.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type HabitMessage = { sizes: { fifteen: string @@ -39,10 +37,10 @@ const _default: Messages = { fifteen: '每十五分钟统计一次', halfHour: '每半小时统计一次', hour: '每一小时统计一次', - twoHour: '每两小时统计一次' + twoHour: '每两小时统计一次', }, average: { - label: '平均每天' + label: '平均每天', }, dateRange: { lastDay: '最近 24 小时', @@ -50,24 +48,24 @@ const _default: Messages = { lastWeek: '最近 7 天', last15Days: '最近 15 天', last30Days: '最近 30 天', - last60Days: '最近 60 天' + last60Days: '最近 60 天', }, chart: { title: '上网习惯统计', saveAsImageTitle: '保存', yAxisMin: '浏览时长 / 分钟', - yAxisHour: '浏览时长 / 小时' - } + yAxisHour: '浏览时长 / 小时', + }, }, zh_TW: { sizes: { fifteen: '按十五分鐘統計', halfHour: '按半小時統計', hour: '按一小時統計', - twoHour: '按兩小時統計' + twoHour: '按兩小時統計', }, average: { - label: '平均每天' + label: '平均每天', }, dateRange: { lastDay: '最近 24 小時', @@ -75,24 +73,24 @@ const _default: Messages = { lastWeek: '最近 7 天', last15Days: '最近 15 天', last30Days: '最近 30 天', - last60Days: '最近 60 天' + last60Days: '最近 60 天', }, chart: { title: '上網習慣統計', saveAsImageTitle: '保存', yAxisMin: '瀏覽時長 / 分鐘', - yAxisHour: '瀏覽時長 / 小時' - } + yAxisHour: '瀏覽時長 / 小時', + }, }, en: { sizes: { fifteen: 'Per 15 minutes', halfHour: 'Per half hour', hour: 'Per one hour', - twoHour: 'Per two hours' + twoHour: 'Per two hours', }, average: { - label: 'Daily average' + label: 'Daily average', }, dateRange: { lastDay: 'Last day', @@ -100,24 +98,24 @@ const _default: Messages = { lastWeek: 'Last week', last15Days: 'Last 15 days', last30Days: 'Last 30 days', - last60Days: 'Last 60 days' + last60Days: 'Last 60 days', }, chart: { title: 'Time-phased Statistics of Browsing Time', saveAsImageTitle: 'Snapshot', yAxisMin: 'Browsing Time / minute', - yAxisHour: 'Browsing Time / hour' - } + yAxisHour: 'Browsing Time / hour', + }, }, ja: { sizes: { fifteen: '15分で統計', halfHour: '30分で統計', hour: '1時間ごとの統計', - twoHour: '2時間ごとの統計' + twoHour: '2時間ごとの統計', }, average: { - label: '1日平均' + label: '1日平均', }, dateRange: { lastDay: '過去24時間', @@ -125,15 +123,15 @@ const _default: Messages = { lastWeek: '先週', last15Days: '過去15日間', last30Days: '過去30日間', - last60Days: '過去60日間' + last60Days: '過去60日間', }, chart: { title: '時系列の統計を閲覧する', saveAsImageTitle: 'ダウンロード', yAxisMin: '閲覧時間/分', - yAxisHour: '閲覧時間/時間' - } - } + yAxisHour: '閲覧時間/時間', + }, + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts new file mode 100644 index 000000000..4ea16502e --- /dev/null +++ b/src/i18n/message/app/help-us.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _AlertLine = + | 'l1' + | 'l2' + | 'l3' + | 'l4' + +export type HelpUsMessage = { + title: string + alert: { [line in _AlertLine]: string } + button: string + loading: string +} + +const _default: Messages = { + zh_CN: { + title: '欢迎一起来改善本地化翻译!', + alert: { + l1: '由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。', + l2: '为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。', + l3: '如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。', + l4: '当某种语言的翻译进度达到 80% 之后,我将会考虑在扩展中支持它。', + }, + button: '前往 Crowdin', + loading: '正在查询翻译进度...', + }, + 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.', + }, + 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...', + }, +} + +export default _default \ No newline at end of file diff --git a/src/app/locale/messages.ts b/src/i18n/message/app/index.ts similarity index 66% rename from src/app/locale/messages.ts rename to src/i18n/message/app/index.ts index 5b52845bc..1a4f4ba46 100644 --- a/src/app/locale/messages.ts +++ b/src/i18n/message/app/index.ts @@ -5,23 +5,24 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" -import itemMessages, { ItemMessage } from "@util/i18n/components/item" -import dataManageMessages, { DataManageMessage } from "./components/data-manage" -import reportMessages, { ReportMessage } from "./components/report" -import trendMessages, { TrendMessage } from "./components/trend" -import menuMessages, { MenuMessage } from "./components/menu" -import habitMessages, { HabitMessage } from "./components/habit" -import limitMessages, { LimitMessage } from "./components/limit" -import optionMessages, { OptionMessage } from "./components/option" -import whitelistMessages, { WhitelistMessage } from "./components/whitelist" -import mergeRuleMessages, { MergeRuleMessage } from "./components/merge-rule" -import siteManageManages, { SiteManageMessage } from "./components/site-manage" -import operationMessages, { OperationMessage } from './components/operation' -import confirmMessages, { ConfirmMessage } from './components/confirm' -import dashboardMessages, { DashboardMessage } from "./components/dashboard" -import timeFormatMessages, { TimeFormatMessage } from "./components/time-format" -import calendarMessages, { CalendarMessage } from "@util/i18n/components/calendar" +import itemMessages, { ItemMessage } from "@i18n/message/common/item" +import dataManageMessages, { DataManageMessage } from "./data-manage" +import reportMessages, { ReportMessage } from "./report" +import trendMessages, { TrendMessage } from "./trend" +import menuMessages, { MenuMessage } from "./menu" +import habitMessages, { HabitMessage } from "./habit" +import limitMessages, { LimitMessage } from "./limit" +import optionMessages, { OptionMessage } from "./option" +import whitelistMessages, { WhitelistMessage } from "./whitelist" +import mergeRuleMessages, { MergeRuleMessage } from "./merge-rule" +import siteManageManages, { SiteManageMessage } from "./site-manage" +import operationMessages, { OperationMessage } from './operation' +import confirmMessages, { ConfirmMessage } from './confirm' +import dashboardMessages, { DashboardMessage } from "./dashboard" +import timeFormatMessages, { TimeFormatMessage } from "./time-format" +import calendarMessages, { CalendarMessage } from "@i18n/message/common/calendar" +import popupDurationMessages, { PopupDurationMessage } from "@i18n/message/common/popup-duration" +import helpUsMessages, { HelpUsMessage } from "./help-us" export type AppMessage = { dataManage: DataManageMessage @@ -40,6 +41,8 @@ export type AppMessage = { dashboard: DashboardMessage calendar: CalendarMessage timeFormat: TimeFormatMessage + duration: PopupDurationMessage + helpUs: HelpUsMessage } const _default: Messages = { @@ -60,6 +63,8 @@ const _default: Messages = { dashboard: dashboardMessages.zh_CN, calendar: calendarMessages.zh_CN, timeFormat: timeFormatMessages.zh_CN, + duration: popupDurationMessages.zh_CN, + helpUs: helpUsMessages.zh_CN, }, zh_TW: { dataManage: dataManageMessages.zh_TW, @@ -78,6 +83,8 @@ const _default: Messages = { dashboard: dashboardMessages.zh_TW, calendar: calendarMessages.zh_TW, timeFormat: timeFormatMessages.zh_TW, + duration: popupDurationMessages.zh_TW, + helpUs: helpUsMessages.zh_TW, }, en: { dataManage: dataManageMessages.en, @@ -96,6 +103,8 @@ const _default: Messages = { dashboard: dashboardMessages.en, calendar: calendarMessages.en, timeFormat: timeFormatMessages.en, + duration: popupDurationMessages.en, + helpUs: helpUsMessages.en, }, ja: { dataManage: dataManageMessages.ja, @@ -113,7 +122,9 @@ const _default: Messages = { confirm: confirmMessages.ja, dashboard: dashboardMessages.ja, calendar: calendarMessages.ja, - timeFormat: timeFormatMessages.ja + timeFormat: timeFormatMessages.ja, + duration: popupDurationMessages.ja, + helpUs: helpUsMessages.ja, } } diff --git a/src/app/locale/components/limit.ts b/src/i18n/message/app/limit.ts similarity index 83% rename from src/app/locale/components/limit.ts rename to src/i18n/message/app/limit.ts index 2e330af1c..8d91c659d 100644 --- a/src/app/locale/components/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -5,11 +5,13 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type LimitMessage = { conditionFilter: string filterDisabled: string + addTitle: string + useWildcard: string + urlPlaceholder: string + item: { condition: string time: string @@ -18,16 +20,14 @@ export type LimitMessage = { delayAllowedInfo: string waste: string operation: string - }, + } button: { add: string test: string paste: string save: string delete: string - }, - addTitle: string - useWildcard: string + } timeUnit: { hour: string minute: string @@ -41,7 +41,6 @@ export type LimitMessage = { deleted: string noPermissionFirefox: string } - urlPlaceholder: string } const _default: Messages = { @@ -55,21 +54,21 @@ const _default: Messages = { enabled: '是否有效', delayAllowed: '再看 5 分钟', delayAllowedInfo: '上网时间超过限制时,点击【再看 5 分钟】短暂延时。如果关闭该功能则不能延时。', - operation: '操作' + operation: '操作', }, button: { add: '新增', test: '网址测试', paste: '粘贴', save: '保存', - delete: '删除' + delete: '删除', }, addTitle: '新增限制', useWildcard: '是否使用通配符', timeUnit: { hour: '小时', minute: '分钟', - second: '秒' + second: '秒', }, message: { saved: '保存成功', @@ -77,9 +76,9 @@ const _default: Messages = { noTime: '未填写每日限制时长', deleteConfirm: '是否删除限制:{cond}?', deleted: '删除成功', - noPermissionFirefox: "请先在插件管理页[about:addons]开启该插件的粘贴板权限" + noPermissionFirefox: '请先在插件管理页[about:addons]开启该插件的粘贴板权限', }, - urlPlaceholder: "请直接粘贴网址 ➡️" + urlPlaceholder: '请直接粘贴网址 ➡️', }, zh_TW: { conditionFilter: '輸入網址,然後回車', @@ -91,21 +90,21 @@ const _default: Messages = { enabled: '是否有效', delayAllowed: '再看 5 分鐘', delayAllowedInfo: '上網時間超過限製時,點擊【再看 5 分鐘】短暫延時。如果關閉該功能則不能延時。', - operation: '操作' + operation: '操作', }, button: { add: '新增', test: '網址測試', paste: '粘貼', save: '保存', - delete: '刪除' + delete: '刪除', }, addTitle: '新增限製', useWildcard: '是否使用通配符', timeUnit: { hour: '小時', minute: '分鐘', - second: '秒' + second: '秒', }, message: { saved: '保存成功', @@ -113,9 +112,9 @@ const _default: Messages = { noTime: '未填冩每日限製時長', deleteConfirm: '是否刪除限製:{cond}?', deleted: '刪除成功', - noPermissionFirefox: "請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限" + noPermissionFirefox: '請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限', }, - urlPlaceholder: "請直接粘貼網址 ➡️" + urlPlaceholder: '請直接粘貼網址 ➡️', }, en: { conditionFilter: 'URL', @@ -127,21 +126,21 @@ const _default: Messages = { enabled: 'Enabled', delayAllowed: 'More 5 minutes', delayAllowedInfo: 'If it times out, allow a temporary delay of 5 minutes', - operation: 'Operations' + operation: 'Operations', }, button: { add: 'New', test: 'Test URL', paste: 'Paste', save: 'Save', - delete: 'Delete' + delete: 'Delete', }, addTitle: 'New', useWildcard: 'Whether to use wildcard', timeUnit: { hour: 'Hours', minute: 'Minutes', - second: 'Seconds' + second: 'Seconds', }, message: { saved: 'Saved successfully', @@ -149,9 +148,9 @@ const _default: Messages = { noTime: 'Unfilled limited time per day', deleteConfirm: 'Do you want to delete the rule of {cond}?', deleted: 'Deleted successfully', - noPermissionFirefox: "Please enable the clipboard permission of this addon on the management page (about:addons) first" + noPermissionFirefox: 'Please enable the clipboard permission of this addon on the management page (about:addons) first', }, - urlPlaceholder: "Please paste the URL directly ➡️" + urlPlaceholder: 'Please paste the URL directly ➡️', }, ja: { conditionFilter: 'URL', @@ -163,21 +162,21 @@ const _default: Messages = { enabled: '有效', delayAllowed: 'さらに5分間閲覧する', delayAllowedInfo: '時間が経過した場合は、一時的に5分遅らせることができます', - operation: '操作' + operation: '操作', }, button: { add: '新增', test: 'テストURL', paste: 'ペースト', save: 'セーブ', - delete: '削除' + delete: '削除', }, addTitle: '新增', useWildcard: 'ワイルドカードを使用するかどうか', timeUnit: { hour: '時間', minute: '分', - second: '秒' + second: '秒', }, message: { noUrl: '埋められていない制限URL', @@ -185,10 +184,10 @@ const _default: Messages = { saved: '正常に保存', deleteConfirm: '{cond} の制限を削除しますか?', deleted: '正常に削除', - noPermissionFirefox: "最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください" + noPermissionFirefox: '最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください', }, - urlPlaceholder: "URLを直接貼り付けてください➡️" - } + urlPlaceholder: 'URLを直接貼り付けてください➡️', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/menu.ts b/src/i18n/message/app/menu.ts similarity index 90% rename from src/app/locale/components/menu.ts rename to src/i18n/message/app/menu.ts index 9c6d27f00..de8bac01d 100644 --- a/src/app/locale/components/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type MenuMessage = { dashboard: string data: string @@ -24,8 +22,9 @@ export type MenuMessage = { other: string feedback: string rate: string - translationMistake: string + helpUs: string } + const _default: Messages = { zh_CN: { dashboard: '仪表盘', @@ -44,7 +43,7 @@ const _default: Messages = { other: '其他', feedback: '有什么反馈吗?', rate: '打个分吧!', - translationMistake: '提交翻译错误' + helpUs: '帮助我们~', }, zh_TW: { dashboard: '儀錶盤', @@ -63,10 +62,10 @@ const _default: Messages = { other: '其他', feedback: '有什麼反饋嗎?', rate: '打個分吧!', - translationMistake: '改善翻译~' + helpUs: '帮助我们~', }, en: { - dashboard: "Dashboard", + dashboard: 'Dashboard', data: 'My Data', dataReport: 'Record', dataHistory: 'Trend', @@ -82,7 +81,7 @@ const _default: Messages = { option: 'Options', feedback: 'Feedback Questionnaire', rate: 'Rate It', - translationMistake: 'Contribute translation' + helpUs: 'Help Us', }, ja: { dashboard: 'ダッシュボード', @@ -101,7 +100,7 @@ const _default: Messages = { option: '拡張設定', feedback: 'フィードバックアンケート', rate: 'それを評価', - translationMistake: 'Contribute translation' + helpUs: 'Help Us', } } diff --git a/src/app/locale/components/merge-rule.ts b/src/i18n/message/app/merge-rule.ts similarity index 92% rename from src/app/locale/components/merge-rule.ts rename to src/i18n/message/app/merge-rule.ts index f33fde9ba..2b2ae37b2 100644 --- a/src/app/locale/components/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -5,7 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" export type MergeRuleMessage = { resultOfOrigin: string resultOfLevel: string @@ -40,7 +39,7 @@ const _default: Messages = { infoAlert2: '合并后域名可填具体的域名,或者填数字,或者不填', infoAlert3: '如果填数字,则表示合并后域名的级数。比如存在规则【 *.*.edu.cn >>> 3 】,那么 www.hust.edu.cn 将被合并至 hust.edu.cn', infoAlert4: '如果不填,则表示原域名不会被合并', - infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级' + infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级', }, zh_TW: { resultOfOrigin: '不合並', @@ -55,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} 的前一級', }, @@ -72,9 +71,9 @@ const _default: Messages = { infoAlert0: 'Click the [New One] button, the input boxes of the source site and the merge site will be displayed, fill in and save the rule', infoAlert1: 'The origin site can be filled with a specific site or regular expression, such as www.baidu.com, *.baidu.com, *.google.com.*, to determine which sites will match this rule while merging', infoAlert2: 'The merged site can be filled with a specific site, a number or blank', - infoAlert3: 'A number means the level of merged site. For example, there is a rule "*.*.edu.cn >>> 3", then "www.hust.edu.cn" will be merged to "hust.edu.cn"', + infoAlert3: 'A number means the level of merged site. For example, there is a rule \'*.*.edu.cn >>> 3\', then \'www.hust.edu.cn\' will be merged to \'hust.edu.cn\'', infoAlert4: 'Blank means the origin site will not be merged', - infoAlert5: 'If no rule is matched, it will default to the level before {psl}' + infoAlert5: 'If no rule is matched, it will default to the level before {psl}', }, ja: { resultOfOrigin: '不合并', @@ -89,10 +88,10 @@ 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} より前のレベルになります' - } + infoAlert5: '一致するルールがない場合、デフォルトで {psl} より前のレベルになります', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/operation.ts b/src/i18n/message/app/operation.ts similarity index 78% rename from src/app/locale/components/operation.ts rename to src/i18n/message/app/operation.ts index 0e3362820..7eea0c7d5 100644 --- a/src/app/locale/components/operation.ts +++ b/src/i18n/message/app/operation.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type OperationMessage = { confirmTitle: string successMsg: string @@ -14,31 +12,31 @@ export type OperationMessage = { newOne: string } -const messages: Messages = { +const _default: Messages = { zh_CN: { confirmTitle: '操作确认', successMsg: '操作成功!', newOne: '新增', - save: '保存' + save: '保存', }, zh_TW: { confirmTitle: '操作確認', successMsg: '操作成功!', newOne: '新增', - save: '保存' + save: '保存', }, en: { confirmTitle: 'Confirm', successMsg: 'Successfully!', newOne: 'New One', - save: 'Save' + save: 'Save', }, ja: { confirmTitle: '動作確認', successMsg: '正常に動作しました!', newOne: '追加', - save: '保存' - } + save: '保存', + }, } -export default messages \ No newline at end of file +export default _default \ No newline at end of file diff --git a/src/app/locale/components/option.ts b/src/i18n/message/app/option.ts similarity index 74% rename from src/app/locale/components/option.ts rename to src/i18n/message/app/option.ts index d5f04a802..54be89966 100644 --- a/src/app/locale/components/option.ts +++ b/src/i18n/message/app/option.ts @@ -5,9 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" -import popupDurationMessages, { PopupDurationMessage } from "@util/i18n/components/popup-duration" - export type OptionMessage = { yes: string no: string @@ -17,7 +14,6 @@ export type OptionMessage = { defaultMergeDomain: string defaultDisplay: string displaySiteName: string - duration: PopupDurationMessage durationWidth: string weekStart: string weekStartAsNormal: string @@ -98,11 +94,10 @@ const _default: Messages = { title: '弹窗页', max: '只显示前 {input} 条数据,剩下的条目合并显示', defaultMergeDomain: '{input} 打开时合并子域名', - defaultDisplay: "打开时显示 {duration} {type}", + defaultDisplay: '打开时显示 {duration} {type}', displaySiteName: '{input} 显示时是否使用 {siteName} 来代替域名', - duration: popupDurationMessages.zh_CN, - durationWidth: "80px", - weekStart: "每周的第一天 {input}", + durationWidth: '80px', + weekStart: '每周的第一天 {input}', weekStartAsNormal: '按照惯例', }, appearance: { @@ -114,29 +109,29 @@ const _default: Messages = { icon: '扩展图标', badgeTextContent: '当前网站的今日浏览时长', locale: { - label: "语言设置 {input}", - default: FOLLOW_BROWSER.zh_CN, - changeConfirm: "语言设置成功,请刷新页面!", - reloadButton: "刷新" + label: '语言设置 {input}', + default: '跟随浏览器', + changeConfirm: '语言设置成功,请刷新页面!', + reloadButton: '刷新', }, printInConsole: { label: '{input} 是否在 {console} 里打印当前网站的 {info}', console: '浏览器的控制台', - info: '今日访问信息' + info: '今日访问信息', }, darkMode: { - label: "夜间模式 {input}", + label: '夜间模式 {input}', options: { - default: FOLLOW_BROWSER.zh_CN, - on: "始终开启", - off: "始终关闭", - timed: "定时开启" - } + default: '跟随浏览器', + on: '始终开启', + off: '始终关闭', + timed: '定时开启', + }, }, limitFilterType: { - label: "每日时限的背景风格 {input}", - translucent: "半透明", - groundGlass: "毛玻璃", + label: '每日时限的背景风格 {input}', + translucent: '半透明', + groundGlass: '毛玻璃', }, }, statistics: { @@ -149,7 +144,7 @@ const _default: Messages = { localFilesInfo: '支持 PDF、图片、txt 以及 json 等格式', collectSiteName: '{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}', siteName: '网站的名称', - siteNameUsage: '数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称' + siteNameUsage: '数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称', }, backup: { title: '数据备份', @@ -158,21 +153,21 @@ const _default: Messages = { meta: { none: { label: '不开启备份', - auth: '' + auth: '', }, gist: { label: 'Github Gist', auth: 'Personal Access Token {info} {input}', - authInfo: '需要创建一个至少包含 gist 权限的 token' - } + authInfo: '需要创建一个至少包含 gist 权限的 token', + }, }, alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', test: '测试', - operation: '备份数据' + operation: '备份数据', }, resetButton: '恢复默认', resetSuccess: '成功重置为默认值', - defaultValue: '默认值: {default}' + defaultValue: '默认值: {default}', }, zh_TW: { yes: '是', @@ -181,11 +176,10 @@ const _default: Messages = { title: '彈窗頁', max: '隻顯示前 {input} 條數據,剩下的條目合並顯示', defaultMergeDomain: '{input} 打開時合併子域名', - defaultDisplay: "打開時顯示 {duration} {type}", + defaultDisplay: '打開時顯示 {duration} {type}', displaySiteName: '{input} 顯示時是否使用 {siteName} 來代替域名', - duration: popupDurationMessages.zh_CN, - durationWidth: "80px", - weekStart: "每週的第一天 {input}", + durationWidth: '80px', + weekStart: '每週的第一天 {input}', weekStartAsNormal: '按照慣例', }, appearance: { @@ -197,29 +191,29 @@ const _default: Messages = { icon: '擴充圖標', badgeTextContent: '當前網站的今日瀏覽時長', locale: { - label: "語言設置 {input}", - default: FOLLOW_BROWSER.zh_TW, - changeConfirm: "語言設置成功,請刷新頁麵!", - reloadButton: "刷新" + label: '語言設置 {input}', + default: '跟隨瀏覽器', + changeConfirm: '語言設置成功,請刷新頁麵!', + reloadButton: '刷新', }, printInConsole: { label: '{input} 是否在 {console} 裡打印當前網站的 {info}', console: '瀏覽器控制台', - info: '今日拜訪信息' + info: '今日拜訪信息', }, darkMode: { - label: "黑暗模式 {input}", + label: '黑暗模式 {input}', options: { - default: FOLLOW_BROWSER.zh_TW, - on: "始終開啟", - off: "始終關閉", - timed: "定時開啟" - } + default: '跟隨瀏覽器', + on: '始終開啟', + off: '始終關閉', + timed: '定時開啟', + }, }, limitFilterType: { - label: "每日時限的背景風格 {input}", - translucent: "半透明", - groundGlass: "毛玻璃", + label: '每日時限的背景風格 {input}', + translucent: '半透明', + groundGlass: '毛玻璃', }, }, statistics: { @@ -232,7 +226,7 @@ const _default: Messages = { localFilesInfo: '支持 PDF、圖片、txt 以及 json 等格式', collectSiteName: '{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}', siteName: '網站的名稱', - siteNameUsage: '數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱' + siteNameUsage: '數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱', }, backup: { title: '數據備份', @@ -240,21 +234,21 @@ const _default: Messages = { client: '客戶端標識 {input}', meta: { none: { - label: '關閉備份' + label: '關閉備份', }, gist: { label: 'Github Gist', auth: 'Personal Access Token {info} {input}', authInfo: '需要創建一個至少包含 gist 權限的 token', - } + }, }, alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', test: '測試', - operation: '備份數據' + operation: '備份數據', }, resetButton: '恢複默認', resetSuccess: '成功重置爲默認值', - defaultValue: '默認值: {default}' + defaultValue: '默認值: {default}', }, en: { yes: 'Yes', @@ -263,12 +257,11 @@ const _default: Messages = { title: 'Popup Page', max: 'Show the first {input} data items', defaultMergeDomain: '{input} Whether to merge subdomains on open', - defaultDisplay: "Show {duration} {type} on open", + defaultDisplay: 'Show {duration} {type} on open', displaySiteName: '{input} Whether to display {siteName} instead of URL', - duration: popupDurationMessages.en, - durationWidth: "110px", + durationWidth: '110px', weekStart: 'The first day for each week {input}', - weekStartAsNormal: 'As Normal' + weekStartAsNormal: 'As Normal', }, appearance: { title: 'Appearance', @@ -279,29 +272,29 @@ const _default: Messages = { icon: 'the icon of extension', badgeTextContent: 'the browsing time of current website', locale: { - label: "Language {input}", - default: FOLLOW_BROWSER.en, - changeConfirm: "The language has been changed successfully, please reload this page!", - reloadButton: "Reload" + label: 'Language {input}', + default: 'Follow browser', + changeConfirm: 'The language has been changed successfully, please reload this page!', + reloadButton: 'Reload', }, printInConsole: { label: '{input} Whether to print {info} in the {console}', console: 'console', - info: 'the visit count of the current website today' + info: 'the visit count of the current website today', }, darkMode: { - label: "Dark mode {input}", + label: 'Dark mode {input}', options: { - default: FOLLOW_BROWSER.en, - on: "Always on", - off: "Always off", - timed: "Timed on" - } + default: 'Follow browser', + on: 'Always on', + off: 'Always off', + timed: 'Timed on', + }, }, limitFilterType: { - label: "Background style for daily time limit {input}", - translucent: "Translucent", - groundGlass: "Ground Glass", + label: 'Background style for daily time limit {input}', + translucent: 'Translucent', + groundGlass: 'Ground Glass', }, }, statistics: { @@ -312,10 +305,9 @@ const _default: Messages = { countLocalFiles: '{input} Whether to count the time to {localFileTime} {info} in the browser', localFileTime: ' read a local file ', localFilesInfo: 'Supports files of types such as PDF, image, txt and json', - collectSiteName: '{input} Whether to automatically collect {siteName} when visiting the site homepage', + collectSiteName: '{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage', siteName: ' the site name ', - siteNameUsage: 'The data is only stored locally and will be displayed instead of the URL to increase the recognition.' - + 'Of course, you can also customize the name of each site.' + siteNameUsage: 'The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site.', }, backup: { title: 'Data Backup', @@ -323,13 +315,13 @@ const _default: Messages = { client: 'Client name {input}', meta: { none: { - label: 'Always off' + label: 'Always off', }, gist: { label: 'Github Gist', auth: 'Personal Access Token {info} {input}', authInfo: 'One token with at least gist permission is required', - } + }, }, alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', test: 'Test', @@ -337,7 +329,7 @@ const _default: Messages = { }, resetButton: 'Reset', resetSuccess: 'Reset to default successfully!', - defaultValue: 'Default: {default}' + defaultValue: 'Default: {default}', }, ja: { yes: 'はい', @@ -346,10 +338,9 @@ const _default: Messages = { title: 'ポップアップページ', max: '最初の {input} 個のデータのみを表示し、残りのエントリは結合されます', defaultMergeDomain: '{input} オープン時にサブドメインをマージ', - defaultDisplay: "開くと {duration} {type} が表示されます", - displaySiteName: '{input} ホストの代わりに {siteName} を表示するかどうか', - duration: popupDurationMessages.ja, - durationWidth: "100px", + defaultDisplay: '開くと {duration} {type} が表示されます', + displaySiteName: '{input} ホストの代わりに {siteName} {siteNameUsage} を表示するかどうか', + durationWidth: '100px', weekStart: '週の最初の日 {input}', weekStartAsNormal: 'いつものように', }, @@ -362,30 +353,30 @@ const _default: Messages = { icon: '拡張機能のアイコン', badgeTextContent: '現在のウェブサイトの閲覧時間', locale: { - label: "言語設定 {input}", - default: FOLLOW_BROWSER.ja, - changeConfirm: "言語が正常に変更されました。このページをリロードしてください。", - reloadButton: "リロード" + label: '言語設定 {input}', + default: 'ブラウザと同じ', + changeConfirm: '言語が正常に変更されました。このページをリロードしてください。', + reloadButton: 'リロード', }, printInConsole: { label: '{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか', console: 'コンソール', - info: '今日の情報をご覧ください' + info: '今日の情報をご覧ください', }, darkMode: { - label: "ダークモード {input}", + label: 'ダークモード {input}', options: { - default: FOLLOW_BROWSER.ja, - on: "常にオン", - off: "常にオフ", - timed: "時限スタート" - } + default: 'ブラウザと同じ', + on: '常にオン', + off: '常にオフ', + timed: '時限スタート', + }, }, limitFilterType: { - label: "毎日の時間制限の背景スタイル {input}", - translucent: "半透明", - groundGlass: "すりガラス", - } + label: '毎日の時間制限の背景スタイル {input}', + translucent: '半透明', + groundGlass: 'すりガラス', + }, }, statistics: { title: '統計', @@ -397,8 +388,7 @@ const _default: Messages = { localFilesInfo: 'PDF、画像、txt、jsonを含む', collectSiteName: '{input} ウェブサイトのホームページにアクセスしたときにウェブサイトの名前を自動的に収集するかどうか', siteName: 'サイト名', - siteNameUsage: 'データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。' - + 'もちろん、各Webサイトの名前をカスタマイズできます。' + siteNameUsage: 'データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。', }, backup: { title: 'データバックアップ', @@ -406,21 +396,22 @@ const _default: Messages = { client: 'クライアント名 {input}', meta: { none: { - label: 'バックアップを有効にしない' + label: 'バックアップを有効にしない', }, gist: { label: 'Github Gist', - auth: 'Personal Access Token {input}' - } + auth: 'Personal Access Token {info} {input}', + authInfo: 'One token with at least gist permission is required', + }, }, alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', test: 'テスト', - operation: 'バックアップ' + operation: 'バックアップ', }, resetButton: 'リセット', resetSuccess: 'デフォルトに正常にリセット', - defaultValue: 'デフォルト値:{default}' - } + defaultValue: 'デフォルト値:{default}', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/report.ts b/src/i18n/message/app/report.ts similarity index 97% rename from src/app/locale/components/report.ts rename to src/i18n/message/app/report.ts index 24bf01ba0..061f9ad01 100644 --- a/src/app/locale/components/report.ts +++ b/src/i18n/message/app/report.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type ReportMessage = { startDate: string endDate: string @@ -59,7 +57,7 @@ const _default: Messages = { remoteReading: { on: '正在查询远端备份数据', off: '单击以开启远端备份数据查询功能', - } + }, }, zh_TW: { startDate: '開始日期', @@ -84,8 +82,8 @@ const _default: Messages = { }, remoteReading: { on: '正在查詢遠端備份數據', - off: '單擊以開啟遠端備份數據查詢功能' - } + off: '單擊以開啟遠端備份數據查詢功能', + }, }, en: { startDate: 'Start date', @@ -110,8 +108,8 @@ const _default: Messages = { }, remoteReading: { on: 'Reading remote backuped data', - off: 'Click to read remote backuped data' - } + off: 'Click to read remote backuped data', + }, }, ja: { startDate: '開始日', @@ -137,8 +135,8 @@ const _default: Messages = { remoteReading: { on: 'リモート バックアップ データのクエリ', off: 'クリックして、リモート バックアップ データのクエリ機能を有効にします', - } - } + }, + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts new file mode 100644 index 000000000..b3c46d264 --- /dev/null +++ b/src/i18n/message/app/site-manage.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type SiteManageMessage = { + hostPlaceholder: string + aliasPlaceholder: string + onlyDetected: string + deleteConfirmMsg: string + column: { + host: string + alias: string + aliasInfo: string + source: string + } + source: { + user: string + detected: string + } + button: { + add: string + delete: string + modify: string + save: string + } + form: { + emptyAlias: string + emptyHost: string + } + msg: { + hostExistWarn: string + saved: string + existedTag: string + mergedTag: string + } +} + +const _default: Messages = { + zh_CN: { + hostPlaceholder: '请输入域名,然后回车', + aliasPlaceholder: '请输入网站名,然后回车', + onlyDetected: '只看自动抓取', + deleteConfirmMsg: '{host} 的名称设置将会被删除', + column: { + host: '网站域名', + alias: '网站名称', + aliasInfo: '网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名', + source: '来源', + }, + source: { + user: '手动设置', + detected: '自动抓取', + }, + button: { + add: '新增', + delete: '删除', + modify: '修改', + save: '保存', + }, + form: { + emptyAlias: '请输入网站名称', + emptyHost: '请输入网站域名', + }, + msg: { + hostExistWarn: '{host} 已经存在', + saved: '已保存', + existedTag: '已存在', + mergedTag: '合并', + }, + }, + zh_TW: { + hostPlaceholder: '請輸入網域,然後回車', + aliasPlaceholder: '請輸入網站名,然後回車', + onlyDetected: '隻看自動抓取', + deleteConfirmMsg: '{host} 的名稱設置將會被刪除', + column: { + host: '網站域名', + alias: '網站名稱', + aliasInfo: '網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域', + source: '來源', + }, + source: { + user: '手動設置', + detected: '自動抓取', + }, + button: { + add: '新增', + delete: '刪除', + modify: '修改', + save: '保存', + }, + form: { + emptyAlias: '請輸入網站名稱', + emptyHost: '請輸入網站域名', + }, + msg: { + hostExistWarn: '{host} 已經存在', + saved: '已保存', + existedTag: '已存在', + mergedTag: '合並', + }, + }, + en: { + hostPlaceholder: 'Partial URL, then enter', + aliasPlaceholder: 'Partial name, then enter', + onlyDetected: 'Only detected', + deleteConfirmMsg: 'The name of {host} will be deleted', + column: { + host: 'Site URL', + alias: 'Site Name', + aliasInfo: 'The site name will be shown on the record page and the popup page', + source: 'Source', + }, + source: { + user: 'user-maintained', + detected: 'auto-detected', + }, + button: { + add: 'New', + delete: 'Delete', + modify: 'Modify', + save: 'Save', + }, + form: { + emptyAlias: 'Please enter site name', + emptyHost: 'Please enter site URL', + }, + msg: { + hostExistWarn: '{host} exists', + saved: 'Saved', + existedTag: 'EXISTED', + mergedTag: 'MERGED', + }, + }, + ja: { + hostPlaceholder: 'ドメイン名で検索', + aliasPlaceholder: 'サイト名で検索', + onlyDetected: '検出されただけ', + deleteConfirmMsg: '{host} の名前が削除されます', + column: { + host: 'サイトのURL', + alias: 'サイト名', + aliasInfo: 'サイト名はレコードページとポップアップページに表示されます', + source: 'ソース', + }, + source: { + user: '手动输入', + detected: 'システム検出', + }, + button: { + add: '追加', + delete: '削除', + modify: '変更', + save: '保存', + }, + form: { + emptyAlias: 'サイト名を入力してください', + emptyHost: 'ドメイン名を入力してください', + }, + msg: { + hostExistWarn: '{host} が存在します', + saved: '保存しました', + existedTag: '既存', + mergedTag: '合并', + }, + }, +} + +export default _default diff --git a/src/app/locale/components/time-format.ts b/src/i18n/message/app/time-format.ts similarity index 81% rename from src/app/locale/components/time-format.ts rename to src/i18n/message/app/time-format.ts index 574d7df86..25055d4e9 100644 --- a/src/app/locale/components/time-format.ts +++ b/src/i18n/message/app/time-format.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type TimeFormatMessage = { [key in timer.app.TimeFormat]: string } const _default: Messages = { @@ -14,26 +12,26 @@ const _default: Messages = { default: '默认时间格式', hour: '按小时显示', minute: '按分钟显示', - second: '按秒显示' + second: '按秒显示', }, zh_TW: { default: '默認時間格式', hour: '按小時顯示', minute: '按分鐘顯示', - second: '按秒顯示' + second: '按秒顯示', }, en: { default: 'Default time format', hour: 'Display in hours', minute: 'Display in minutes', - second: 'Display in seconds' + second: 'Display in seconds', }, ja: { default: 'デフォルトの時間形式', hour: '時間単位で表示', minute: '分単位で表示', - second: '秒単位で表示' - } + second: '秒単位で表示', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/trend.ts b/src/i18n/message/app/trend.ts similarity index 92% rename from src/app/locale/components/trend.ts rename to src/i18n/message/app/trend.ts index 30dc83795..6ceb167e4 100644 --- a/src/app/locale/components/trend.ts +++ b/src/i18n/message/app/trend.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" - export type TrendMessage = { hostPlaceholder: string startDate: string, @@ -42,11 +40,11 @@ const _default: Messages = { minute: '时长 / 分钟', hour: '时长 / 小时', }, - numberUnit: '次' + numberUnit: '次', }, saveAsImageTitle: '保存', defaultSubTitle: '请先在左上角选择需要分析的域名', - merged: '合并' + merged: '合并', }, zh_TW: { hostPlaceholder: '蒐索你想分析的網域', @@ -64,11 +62,11 @@ const _default: Messages = { minute: '時長 / 分鐘', hour: '時長 / 小時', }, - numberUnit: '次' + numberUnit: '次', }, saveAsImageTitle: '保存', defaultSubTitle: '請先在左上角選擇需要分析的網域', - merged: '合並' + merged: '合並', }, en: { hostPlaceholder: 'Search site URL', @@ -86,11 +84,11 @@ const _default: Messages = { minute: 'Time / minute', hour: 'Time / hour', }, - numberUnit: 'Visit Counts' + numberUnit: 'Visit Counts', }, saveAsImageTitle: 'Snapshot', defaultSubTitle: 'Search and select one URL to analyze on the top-left corner, pls', - merged: 'Merged' + merged: 'Merged', }, ja: { hostPlaceholder: 'ドメイン名を検索', @@ -108,12 +106,12 @@ const _default: Messages = { minute: '期間 / 分', hour: '期間 / 時間', }, - numberUnit: '回' + numberUnit: '回', }, saveAsImageTitle: 'ダウンロード', defaultSubTitle: 'まず、左上隅で分析するドメイン名を選択します', - merged: '合并' - } + merged: '合并', + }, } export default _default \ No newline at end of file diff --git a/src/app/locale/components/whitelist.ts b/src/i18n/message/app/whitelist.ts similarity index 92% rename from src/app/locale/components/whitelist.ts rename to src/i18n/message/app/whitelist.ts index eba7ec4c9..255f5091b 100644 --- a/src/app/locale/components/whitelist.ts +++ b/src/i18n/message/app/whitelist.ts @@ -5,7 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" export type WhitelistMessage = { addConfirmMsg: string removeConfirmMsg: string @@ -26,7 +25,7 @@ const _default: Messages = { infoAlert0: '白名单内网站的上网时长和打开次数不会被统计', infoAlert1: '白名单内网站的上网时间也不会被限制', placeholder: '域名', - errorInput: "域名格式错误" + errorInput: '域名格式错误', }, zh_TW: { addConfirmMsg: '{url} 加入白名單後,將不再統計該網站的數據', @@ -36,7 +35,7 @@ const _default: Messages = { infoAlert0: '白名單內網站的上網時長和打開次數不會被統計', infoAlert1: '白名單內網站的上網時間也不會被限製', placeholder: '網域', - errorInput: "網域格式錯誤" + errorInput: '網域格式錯誤', }, en: { addConfirmMsg: '{url} won\'t be counted after added into the whitelist any more.', @@ -46,7 +45,7 @@ const _default: Messages = { infoAlert0: 'Whitelisted sites will not be counted', infoAlert1: 'Whitelisted sites will not be restricted', placeholder: 'Site URL', - errorInput: "Invalid site URL" + errorInput: 'Invalid site URL', }, ja: { addConfirmMsg: '{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。', @@ -56,8 +55,8 @@ const _default: Messages = { infoAlert0: 'ホワイトリストのサイトはカウントされません。', infoAlert1: 'ホワイトリストのサイトは制限されません。', placeholder: 'URL', - errorInput: "無効なURL" - } + errorInput: '無効なURL', + }, } export default _default \ No newline at end of file diff --git a/src/util/i18n/components/app.ts b/src/i18n/message/common/base.ts similarity index 51% rename from src/util/i18n/components/app.ts rename to src/i18n/message/common/base.ts index 7cb150bfb..042b44eeb 100644 --- a/src/util/i18n/components/app.ts +++ b/src/i18n/message/common/base.ts @@ -5,12 +5,7 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - -export type AppMessage = { - name: string - marketName: string - description: string +export type BaseMessage = { currentVersion: string allFunction: string guidePage: string @@ -19,39 +14,27 @@ export type AppMessage = { /** * Use for chrome */ -const _default: Messages = { +const _default: Messages = { en: { - name: 'Timer', - marketName: 'Timer - Running & Browsing Time & Visit count', - description: 'To be the BEST web timer.', currentVersion: 'Version: {version}', allFunction: 'All Functions', guidePage: 'User Manual', }, zh_CN: { - name: '网费很贵', - marketName: '网费很贵 - 上网时间统计', - description: '做最好用的上网时间统计工具。', currentVersion: '版本: v{version}', allFunction: '所有功能', guidePage: '用户手册', }, zh_TW: { - name: '網費很貴', - marketName: '網費很貴 - 上網時間統計', - description: '做最好用的上網時間統計工具。', currentVersion: '版本: v{version}', allFunction: '所有功能', guidePage: '用戶手冊', }, ja: { - name: 'Web時間統計', - marketName: 'Web時間統計', - description: '最高のオンライン時間統計ツールを作成します。', currentVersion: 'バージョン: v{version}', allFunction: 'すべての機能', guidePage: 'ユーザーマニュアル', - } + }, } export default _default \ No newline at end of file diff --git a/src/util/i18n/components/calendar.ts b/src/i18n/message/common/calendar.ts similarity index 91% rename from src/util/i18n/components/calendar.ts rename to src/i18n/message/common/calendar.ts index 715aaada9..4313380cb 100644 --- a/src/util/i18n/components/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - export type CalendarMessage = { weekDays: string months: string @@ -27,12 +25,12 @@ const _default: Messages = { en: { weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{m}/{d}/{y}' + dateFormat: '{m}/{d}/{y}', }, ja: { weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{y}/{m}/{d}' + dateFormat: '{y}/{m}/{d}', }, } diff --git a/src/util/i18n/components/content-script.ts b/src/i18n/message/common/content-script.ts similarity index 92% rename from src/util/i18n/components/content-script.ts rename to src/i18n/message/common/content-script.ts index e82b58d4e..91db29146 100644 --- a/src/util/i18n/components/content-script.ts +++ b/src/i18n/message/common/content-script.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - export type ContentScriptMessage = { openTimesConsoleLog: string usedTimeInConsoleLog: string @@ -26,7 +24,7 @@ const _default: Messages = { timeWithMinute: '{minute} 分 {second} 秒', timeWithSecond: '{second} 秒', timeLimitMsg: '您已被【{appName}】限制上网', - more5Minutes: '再看 5 分钟!!我保证!' + more5Minutes: '再看 5 分钟!!我保证!', }, zh_TW: { openTimesConsoleLog: '今天您打開了 {time} 次 {host}。', @@ -36,7 +34,7 @@ const _default: Messages = { timeWithMinute: '{minute} 分 {second} 秒', timeWithSecond: '{second} 秒', timeLimitMsg: '您已被【{appName}】限製上網', - more5Minutes: '再看 5 分鐘!!我保証!' + more5Minutes: '再看 5 分鐘!!我保証!', }, en: { openTimesConsoleLog: 'You have open {host} for {time} time(s) today.', @@ -46,7 +44,7 @@ const _default: Messages = { timeWithMinute: '{minute} minute(s) {second} second(s)', timeWithSecond: '{second} second(s)', timeLimitMsg: 'You have been restricted by [{appName}]', - more5Minutes: 'More 5 minutes, please!!' + more5Minutes: 'More 5 minutes, please!!', }, ja: { openTimesConsoleLog: '今日、あなたは {host} を {time} 回開きました。', @@ -56,8 +54,8 @@ const _default: Messages = { timeWithMinute: '{minute} 分 {second} 秒', timeWithSecond: '{second} 秒', timeLimitMsg: '【{appName}】によって制限されています', - more5Minutes: 'さらに5分間見てください! ! 約束します!' - } + more5Minutes: 'さらに5分間見てください! ! 約束します!', + }, } export default _default \ No newline at end of file diff --git a/src/util/i18n/components/context-menus.ts b/src/i18n/message/common/context-menus.ts similarity index 73% rename from src/util/i18n/components/context-menus.ts rename to src/i18n/message/common/context-menus.ts index c922900ff..b2187a2b8 100644 --- a/src/util/i18n/components/context-menus.ts +++ b/src/i18n/message/common/context-menus.ts @@ -5,59 +5,46 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." -import chromeBase from "./app" - /** * Used for menu */ export type ContextMenusMessage = { add2Whitelist: string removeFromWhitelist: string - allFunctions: string optionPage: string repoPage: string feedbackPage: string - guidePage: string } const _default: Messages = { zh_CN: { add2Whitelist: '将{host}加入白名单', removeFromWhitelist: '将{host}从白名单移出', - allFunctions: chromeBase.zh_CN.allFunction, optionPage: '扩展选项', repoPage: '源码下载', feedbackPage: '吐槽一下', - guidePage: chromeBase.zh_CN.guidePage, }, zh_TW: { add2Whitelist: '將{host}加入白名單', removeFromWhitelist: '將{host}從白名單移出', - allFunctions: chromeBase.zh_TW.allFunction, optionPage: '擴充選項', repoPage: '源碼下載', feedbackPage: '吐槽一下', - guidePage: chromeBase.zh_TW.guidePage, }, en: { add2Whitelist: 'Add {host} to the whitelist', removeFromWhitelist: 'Remove {host} from the whitelist', - allFunctions: chromeBase.en.allFunction, optionPage: 'Options', repoPage: 'Source Code', feedbackPage: 'Issues', - guidePage: chromeBase.en.guidePage, }, ja: { add2Whitelist: 'ホワイトリスト', removeFromWhitelist: 'ホワイトリストから削除する', - allFunctions: chromeBase.ja.allFunction, optionPage: '拡張設定', repoPage: 'ソースコード', feedbackPage: 'フィードバックの欠如', - guidePage: chromeBase.ja.guidePage, - } + }, } export default _default \ No newline at end of file diff --git a/src/util/i18n/components/initial.ts b/src/i18n/message/common/initial.ts similarity index 92% rename from src/util/i18n/components/initial.ts rename to src/i18n/message/common/initial.ts index d71460215..ab1a7f613 100644 --- a/src/util/i18n/components/initial.ts +++ b/src/i18n/message/common/initial.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - /** * Locales for initial data * @@ -28,7 +26,7 @@ const _default: Messages = { pdf: 'PDF 文件', pic: '图片文件', txt: '文本文件', - } + }, }, zh_TW: { localFile: { @@ -36,7 +34,7 @@ const _default: Messages = { pdf: 'PDF 文件', pic: '圖片文件', txt: '文本文件', - } + }, }, en: { localFile: { @@ -44,7 +42,7 @@ const _default: Messages = { pdf: 'PDF Files', pic: 'Images', txt: 'Text Files', - } + }, }, ja: { localFile: { @@ -52,8 +50,8 @@ const _default: Messages = { pdf: 'PDF', pic: '写真', txt: 'TXT', - } - } + }, + }, } export default _default \ No newline at end of file diff --git a/src/util/i18n/components/item.ts b/src/i18n/message/common/item.ts similarity index 94% rename from src/util/i18n/components/item.ts rename to src/i18n/message/common/item.ts index da002174f..15fc4f98d 100644 --- a/src/util/i18n/components/item.ts +++ b/src/i18n/message/common/item.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import type { Messages } from ".." - export type ItemMessage = { date: string host: string @@ -49,8 +47,8 @@ const _default: Messages = { deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的访问记录将被删除', deleteConfirmMsg: '{url} 在 {date} 的访问记录将被删除', exportWholeData: '导出数据', - importWholeData: '导入数据' - } + importWholeData: '导入数据', + }, }, zh_TW: { date: '日期', @@ -69,8 +67,8 @@ const _default: Messages = { deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的拜訪記錄將被刪除', deleteConfirmMsg: '{url} 在 {date} 的拜訪記錄將被刪除', exportWholeData: '導出數據', - importWholeData: '導入數據' - } + importWholeData: '導入數據', + }, }, en: { date: 'Date', @@ -89,8 +87,8 @@ const _default: Messages = { deleteConfirmMsgRange: 'All records of {url} between {start} and {end} will be deleted!', deleteConfirmMsg: 'The record of {url} on {date} will be deleted!', exportWholeData: 'Export Data', - importWholeData: 'Import Data' - } + importWholeData: 'Import Data', + }, }, ja: { date: '日期', @@ -109,9 +107,9 @@ const _default: Messages = { deleteConfirmMsgRange: '{url} {start} から {end} までの拜訪記録は削除されます', deleteConfirmMsg: '{date} の {url} の拜訪記録は削除されます', exportWholeData: 'インポート', - importWholeData: '書き出す' - } - } + importWholeData: '書き出す', + }, + }, } export default _default diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts new file mode 100644 index 000000000..467ecb5e5 --- /dev/null +++ b/src/i18n/message/common/locale.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +/** + * Meta info of locales + * + * @since 0.8.0 + */ +export type LocaleMessages = { + [locale in timer.Locale | timer.TranslatingLocale]: string +} + +const _default: LocaleMessages = { + zh_CN: '简体中文', + zh_TW: '正體中文', + en: 'English', + ja: '日本語', + en_GB: 'English, UK', + en_US: 'English, US', + pl: 'Polski', + pt: 'Português', + pt_BR: 'Portugues, Brasil', + ko: '한국인', + de: 'Deutsch', + es: 'Español', + ru: 'Русский', +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts new file mode 100644 index 000000000..e5891fdbd --- /dev/null +++ b/src/i18n/message/common/meta.ts @@ -0,0 +1,31 @@ + +export type MetaMessage = { + name: string + marketName: string + description: string +} + +const _default: Messages = { + zh_CN: { + name: '网费很贵', + marketName: '网费很贵 - 上网时间统计', + description: '做最好用的上网时间统计工具。', + }, + zh_TW: { + name: '網費很貴', + marketName: '網費很貴 - 上網時間統計', + description: '做最好用的上網時間統計工具。', + }, + ja: { + name: 'Web時間統計', + marketName: 'Web時間統計', + description: '最高のオンライン時間統計ツールを作成します。', + }, + en: { + name: 'Timer', + marketName: 'Timer - Running & Browsing Time & Visit count', + description: 'To be the BEST web timer.', + }, +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration.ts b/src/i18n/message/common/popup-duration.ts new file mode 100644 index 000000000..2a3a59900 --- /dev/null +++ b/src/i18n/message/common/popup-duration.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type PopupDurationMessage = { [key in timer.popup.Duration]: string } + +const _default: Messages = { + zh_CN: { + today: '今日', + thisWeek: '本周', + thisMonth: '本月', + }, + zh_TW: { + today: '今日', + thisWeek: '本週', + thisMonth: '本月', + }, + en: { + today: 'Today\'s', + thisWeek: 'This Week\'s', + thisMonth: 'This Month\'s', + }, + ja: { + today: '今日の', + thisWeek: '今週の', + thisMonth: '今月の', + }, +} + +export default _default \ No newline at end of file diff --git a/src/guide/locale/messages.ts b/src/i18n/message/guide/index.ts similarity index 60% rename from src/guide/locale/messages.ts rename to src/i18n/message/guide/index.ts index 7947c27a8..399d156aa 100644 --- a/src/guide/locale/messages.ts +++ b/src/i18n/message/guide/index.ts @@ -5,17 +5,20 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" -import layoutMessages, { LayoutMessage } from "./components/layout" -import profileMessages, { ProfileMessage } from "./components/profile" -import usageMessages, { UsageMessage } from "./components/usage" -import privacyMessages, { PrivacyMessage } from "./components/privacy" +import layoutMessages, { LayoutMessage } from "./layout" +import profileMessages, { ProfileMessage } from "./profile" +import usageMessages, { UsageMessage } from "./usage" +import privacyMessages, { PrivacyMessage } from "./privacy" +import metaMessages, { MetaMessage } from "../common/meta" +import baseMessages, { BaseMessage } from "../common/base" export type GuideMessage = { layout: LayoutMessage profile: ProfileMessage usage: UsageMessage privacy: PrivacyMessage + meta: MetaMessage + base: BaseMessage } const _default: Messages = { @@ -24,24 +27,32 @@ const _default: Messages = { profile: profileMessages.zh_CN, usage: usageMessages.zh_CN, privacy: privacyMessages.zh_CN, + meta: metaMessages.zh_CN, + base: baseMessages.zh_CN, }, zh_TW: { layout: layoutMessages.zh_TW, profile: profileMessages.zh_TW, usage: usageMessages.zh_TW, privacy: privacyMessages.zh_TW, + meta: metaMessages.zh_TW, + base: baseMessages.zh_TW, }, en: { layout: layoutMessages.en, profile: profileMessages.en, usage: usageMessages.en, privacy: privacyMessages.en, + meta: metaMessages.en, + base: baseMessages.en, }, ja: { layout: layoutMessages.ja, profile: profileMessages.ja, usage: usageMessages.ja, privacy: privacyMessages.ja, + meta: metaMessages.ja, + base: baseMessages.ja, }, } diff --git a/src/guide/locale/components/layout.ts b/src/i18n/message/guide/layout.ts similarity index 74% rename from src/guide/locale/components/layout.ts rename to src/i18n/message/guide/layout.ts index e590332da..5552e96eb 100644 --- a/src/guide/locale/components/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -5,11 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" -import chromeBase from "@util/i18n/components/app" - export type LayoutMessage = { - title: string menu: { profile: string usage: { @@ -28,9 +24,8 @@ export type LayoutMessage = { const _default: Messages = { zh_CN: { - title: chromeBase.zh_CN.guidePage + ' | ' + chromeBase.zh_CN.name, menu: { - profile: '欢迎安装' + chromeBase.zh_CN.name, + profile: '欢迎安装{appName}', usage: { title: '如何使用', quickstart: '快速开始', @@ -41,13 +36,12 @@ const _default: Messages = { title: '隐私声明', scope: '收集哪些数据', storage: '如何处理这些数据', - } + }, }, }, zh_TW: { - title: chromeBase.zh_TW.guidePage + ' | ' + chromeBase.zh_TW.name, menu: { - profile: '歡迎安裝' + chromeBase.zh_TW.name, + profile: '歡迎安裝{appName}', usage: { title: '如何使用', quickstart: '快速開始', @@ -62,26 +56,24 @@ const _default: Messages = { }, }, en: { - title: chromeBase.en.guidePage + ' | ' + chromeBase.en.name, menu: { - profile: 'Welcome to install ' + chromeBase.en.name, + profile: 'Welcome to install {appName}', usage: { title: 'Using Timer', quickstart: 'Quickstart', background: 'Using all functions', - advanced: 'Advanced features' + advanced: 'Advanced features', }, privacy: { title: 'Privary Policy', scope: 'Personal data collected', storage: 'How to do with this data', - } - } + }, + }, }, ja: { - title: chromeBase.ja.guidePage + ' | ' + chromeBase.ja.name, menu: { - profile: chromeBase.ja.name + 'へようこそ', + profile: '{appName}へようこそ', usage: { title: '使い方', quickstart: 'クイックスタート', @@ -92,9 +84,9 @@ const _default: Messages = { title: 'ポリシーと規約', scope: '収集する情報', storage: 'このデータをどうするか', - } - } - } + }, + }, + }, } export default _default \ No newline at end of file diff --git a/src/guide/locale/components/privacy.ts b/src/i18n/message/guide/privacy.ts similarity index 74% rename from src/guide/locale/components/privacy.ts rename to src/i18n/message/guide/privacy.ts index e8a7ab672..4517bfd8f 100644 --- a/src/guide/locale/components/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -6,8 +6,6 @@ */ -import { Messages } from "@util/i18n" - type _StoreKey = | 'p1' | 'p2' @@ -27,43 +25,42 @@ export type PrivacyMessage = { const _default: Messages = { zh_CN: { scope: { - p1: "为了向您提供完整的服务,该扩展在使用过程中会收集您以下的个人信息:", - l1: "1. 您浏览网站的时间,以及访问每个网站的次数。", - l2: "2. 网站的标题以及图标 URL。", - l3: "3. 为了提高用户体验,扩展在必要时会征得您的授权之后,读取您的剪切板内容。", + p1: '为了向您提供完整的服务,该扩展在使用过程中会收集您以下的个人信息:', + l1: '1. 您浏览网站的时间,以及访问每个网站的次数。', + l2: '2. 网站的标题以及图标 URL。', + l3: '3. 为了提高用户体验,扩展在必要时会征得您的授权之后,读取您的剪切板内容。', }, storage: { - p1: "我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。", - p2: "不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。", - p3: "我们只帮助您收集数据,但处置权一定在您。", - } + p1: '我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。', + p2: '不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。', + p3: '我们只帮助您收集数据,但处置权一定在您。', + }, }, zh_TW: { scope: { p1: '為了向您提供完整的服務,該擴展在使用過程中會收集您以下的個人信息:', l1: '1. 您瀏覽網站的時間,以及訪問每個網站的次數。', l2: '2. 網站的標題以及圖標 URL。', - l3: '3. 為了提高用戶體驗,擴展在必要時會徵得您的授權之後,讀取您的剪切板內容。' + l3: '3. 為了提高用戶體驗,擴展在必要時會徵得您的授權之後,讀取您的剪切板內容。', }, storage: { p1: '我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。', p2: '不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。', - p3: '我們只幫助您收集數據,但處置權一定在您。' - } + p3: '我們只幫助您收集數據,但處置權一定在您。', + }, }, en: { scope: { p1: 'In order to provide you with complete services, this extension will collect your following personal information during use:', l1: '1. How long you browse the site, and how many times you visit each site.', l2: '2. The title of the website and the URL of the icon.', - l3: '3. In order to improve user experience, the extension will read your clipboard content after obtaining your authorization when necessary.' + l3: '3. In order to improve user experience, the extension will read your clipboard content after obtaining your authorization when necessary.', }, 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.', - } + }, }, ja: { scope: { @@ -74,11 +71,10 @@ const _default: Messages = { }, storage: { p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', - p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 \ - この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', + p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', - } - } + }, + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/profile.ts b/src/i18n/message/guide/profile.ts new file mode 100644 index 000000000..e0d02b024 --- /dev/null +++ b/src/i18n/message/guide/profile.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _key = + | 'p1' + | 'p2' + +export type ProfileMessage = { + [key in _key]: string +} + +const _default: Messages = { + zh_CN: { + p1: '{appName}是一款开源、免费、用户友好的,用于统计上网时间的浏览器扩展。您可以在 {github} 上查阅它的源代码。', + p2: '这个页面将会告诉您如何使用它,以及相关的隐私政策。', + }, + zh_TW: { + p1: '{appName}是一款開源、免費、用戶友好的,用於統計上網時間的瀏覽器擴展。您可以在 {github} 上查閱它的源代碼。', + 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}.', + p2: 'This page will tell you how to use it, and the related privacy policy.', + }, + ja: { + p1: '{appName}は、オンラインで費やした時間をカウントするための、オープン ソースで無料のユーザー フレンドリーなブラウザ拡張機能です。 {github} でソース コードを確認できます。 ', + p2: 'このページでは、使用方法と関連するプライバシー ポリシーについて説明します。', + }, +} + +export default _default \ No newline at end of file diff --git a/src/guide/locale/components/usage.ts b/src/i18n/message/guide/usage.ts similarity index 77% rename from src/guide/locale/components/usage.ts rename to src/i18n/message/guide/usage.ts index d81ebae54..6d47d9e49 100644 --- a/src/guide/locale/components/usage.ts +++ b/src/i18n/message/guide/usage.ts @@ -1,5 +1,9 @@ -import type { Messages } from "@util/i18n" -import chromeBase from "@util/i18n/components/app" +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ type _QuickstartKey = | 'p1' @@ -32,21 +36,21 @@ export type UsageMessage = { advanced: { [key in _AdvancedKey]: string } } -const messages: Messages = { +const _default: Messages = { zh_CN: { quickstart: { p1: '首先,您可以通过以下几步,开始体验这个扩展:', l1: '1. 将扩展的图标固定在浏览器的右上角,具体的操作方法根据您的浏览器而定。该步骤不会影响扩展的正常运行,但是将极大改善您的交互体验。', l2: '2. 打开任意一个网站,浏览几秒钟,这时您会观察到右上角的图标上有数字跳动,它显示您今天花了多少时间浏览当前网站。', l3: '3. 点击扩展的图标,会弹出一个页面,展示今天或最近一段时间您的上网数据。', - p2: '需要提醒的是,由于时长数据是实时统计,所以安装扩展之前的浏览记录不会被记录。' + p2: '需要提醒的是,由于时长数据是实时统计,所以安装扩展之前的浏览记录不会被记录。', }, background: { p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的 {background}。进入后台页有以下两种方式:', - l1: '1. 您可以右击扩展的图标,在弹出的菜单中点击【' + chromeBase.zh_CN.allFunction + '】。', - l2: '2. 您在图标弹出页的下方也可以找到【' + chromeBase.zh_CN.allFunction + '】链接,同样点击它即可。', + l1: '1. 您可以右击扩展的图标,在弹出的菜单中点击【{allFunction}】。', + l2: '2. 您在图标弹出页的下方也可以找到【{allFunction}】链接,同样点击它即可。', p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。', - backgroundPage: '后台页' + backgroundPage: '后台页', }, advanced: { p1: '这个扩展的核心功能是统计您在不同网站上的浏览行为。此外,它也提供了很多高级功能,来满足您更多的需求。当然,所有的功能您都可以在后台页里找到。', @@ -57,8 +61,8 @@ const messages: Messages = { l5: '5. 它支持将几个相关的网站合并统计到同一个条目,您可以自定义合并的规则。默认按照 {psl} 合并。', l6: '6. 它支持限制每个网站的每日浏览时长,需要您手动添加限制规则。', l7: '7. 它支持夜间模式,同样需要在选项里启用。', - l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。' - } + l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。', + }, }, zh_TW: { quickstart: { @@ -70,8 +74,8 @@ const messages: Messages = { }, background: { p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的 {background}。進入後台頁有以下兩種方式:', - l1: '1. 您可以右擊擴展的圖標,在彈出的菜單中點擊【' + chromeBase.zh_TW.allFunction + '】。', - l2: '2. 您在圖標彈出頁的下方也可以找到【' + chromeBase.zh_TW.allFunction + '】鏈接,同樣點擊它即可。', + l1: '1. 您可以右擊擴展的圖標,在彈出的菜單中點擊【{allFunction}】。', + l2: '2. 您在圖標彈出頁的下方也可以找到【{allFunction}】鏈接,同樣點擊它即可。', p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', backgroundPage: '後台頁', }, @@ -85,66 +89,52 @@ const messages: Messages = { l6: '6. 它支持限制每個網站的每日瀏覽時長,需要您手動添加限制規則。', l7: '7. 它支持夜間模式,同樣需要在選項裡啟用。', l8: '8. 它支持使用 Github Gist 作為雲端存儲多個瀏覽器的數據,並進行聚合查詢。需要您準備一個至少包含 gist 權限的 token。', - } + }, }, 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:', - l1: '1. You can right-click the icon of the extension, and click [' + chromeBase.en.allFunction + '] in the pop-up menu.', - l2: '2. You can also find the [' + chromeBase.en.allFunction + '] link at the bottom of the icon popup page, just click it.', + 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.', l6: '6. It supports limiting the daily browsing time of each website, which requires you to manually add limiting rules.', l7: '7. It supports night mode, which also needs to be enabled in the options.', 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.', - } + }, }, ja: { quickstart: { p1: 'まず、次の手順に従って、この拡張機能の調査を開始できます。', - l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 \ - この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', - l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。\ - これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', + l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', + l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。 これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', l3: '3. 拡張機能のアイコンをクリックすると、ページがポップアップし、今日または最近のインターネット データが表示されます。', p2: 'なお、継続時間データはリアルタイムでカウントされるため、拡張機能をインストールする前の閲覧履歴は記録されません。', }, background: { - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 \ - ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 \ - バックグラウンド ページに入る方法は 2 つあります。', - l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [' + chromeBase.ja.allFunction + '] をクリックします。', - l2: '2. また、アイコン ポップアップ ページの下部に [' + chromeBase.ja.allFunction + '] リンクがあり、それをクリックするだけです。', + p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', + l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [{allFunction}] をクリックします。', + l2: '2. また、アイコン ポップアップ ページの下部に [{allFunction}] リンクがあり、それをクリックするだけです。', p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', backgroundPage: '背景ページ', }, advanced: { - p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 \ - さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 \ - もちろん、バックグラウンド ページですべての機能を見つけることができます。', + p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', l1: '1. 一定期間の同じ Web サイトへのアクセスの傾向を分析し、折れ線グラフで表示できます。', l2: '2. あなたのネットサーフィン頻度を毎日異なる時間帯でカウントし、ヒストグラムで表示できます。 データはサイトにとらわれず、最小の統計粒度は 15 分です。', l3: '3. ローカル ファイルの読み取り時間をカウントできますが、この機能はオプションで有効にする必要があります。', @@ -152,10 +142,9 @@ const messages: Messages = { l5: '5. 複数の関連 Web サイトの統計を同じエントリにマージすることをサポートし、マージのルールをカスタマイズできます。 デフォルトでは {psl} でマージします。', l6: '6. 各 Web サイトの毎日の閲覧時間の制限をサポートしています。これには、制限ルールを手動で追加する必要があります。', l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', - l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 \ - 少なくとも gist 権限を持つトークンを準備する必要があります。', - } - } + l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', + }, + }, } -export default messages \ No newline at end of file +export default _default \ No newline at end of file diff --git a/src/popup/locale/messages.ts b/src/i18n/message/popup/chart.ts similarity index 64% rename from src/popup/locale/messages.ts rename to src/i18n/message/popup/chart.ts index e7ed9ce6b..38cd104bf 100644 --- a/src/popup/locale/messages.ts +++ b/src/i18n/message/popup/chart.ts @@ -5,15 +5,9 @@ * https://opensource.org/licenses/MIT */ -import { Messages } from "@util/i18n" -import chromeBase from "@util/i18n/components/app" -import itemMessages, { ItemMessage } from "@util/i18n/components/item" -import popupDurationMessages, { PopupDurationMessage } from "@util/i18n/components/popup-duration" - -export type PopupMessage = { +export type ChartMessage = { title: { [key in timer.popup.Duration]: string } mergeHostLabel: string - viewMore: string fileName: string saveAsImageTitle: string restoreTitle: string @@ -26,21 +20,16 @@ export type PopupMessage = { updateVersion: string updateVersionInfo: string updateVersionInfo4Firefox: string - currentVersion: string - appName: string - item: ItemMessage - timeDuration: PopupDurationMessage } -const _default: Messages = { +const _default: Messages = { zh_CN: { title: { - today: "今日数据", - thisWeek: "本周数据", - thisMonth: "本月数据" + today: '今日数据', + thisWeek: '本周数据', + thisMonth: '本月数据', }, mergeHostLabel: '合并子域名', - viewMore: chromeBase.zh_CN.allFunction, fileName: '上网时长清单_{today}_by_{app}', saveAsImageTitle: '保存', restoreTitle: '刷新', @@ -53,19 +42,14 @@ const _default: Messages = { updateVersion: '版本升级', updateVersionInfo: '最新版本:{version}', updateVersionInfo4Firefox: '新版本 {version} 已发布\n\n您可以前往插件管理页进行更新', - currentVersion: chromeBase.zh_CN.currentVersion, - appName: chromeBase.zh_CN.name, - item: itemMessages.zh_CN, - timeDuration: popupDurationMessages.zh_CN }, zh_TW: { title: { - today: "今日數據", - thisWeek: "本週數據", - thisMonth: "本月數據" + today: '今日數據', + thisWeek: '本週數據', + thisMonth: '本月數據', }, mergeHostLabel: '合並子網域', - viewMore: chromeBase.zh_TW.allFunction, fileName: '上網時長清單_{today}_by_{app}', saveAsImageTitle: '保存', restoreTitle: '刷新', @@ -78,19 +62,14 @@ const _default: Messages = { updateVersion: '版本昇級', updateVersionInfo: '最新版本:{version}', updateVersionInfo4Firefox: '新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新', - currentVersion: chromeBase.zh_TW.currentVersion, - appName: chromeBase.zh_TW.name, - item: itemMessages.zh_TW, - timeDuration: popupDurationMessages.zh_TW }, en: { title: { today: 'Today\'s Data', - thisWeek: "This Week\'s Data", - thisMonth: "This Month\'s Data" + thisWeek: 'This Week\'s Data', + thisMonth: 'This Month\'s Data', }, mergeHostLabel: 'Merge Sites', - viewMore: chromeBase.en.allFunction, fileName: 'Web_Time_List_{today}_By_{app}', saveAsImageTitle: 'Snapshot', restoreTitle: 'Restore', @@ -103,19 +82,14 @@ const _default: Messages = { updateVersion: 'Updatable', updateVersionInfo: 'Latest: {version}', updateVersionInfo4Firefox: 'Upgrade to {version} in the management page, about:addons, pls', - currentVersion: chromeBase.en.currentVersion, - appName: chromeBase.en.name, - item: itemMessages.en, - timeDuration: popupDurationMessages.en }, ja: { title: { - today: "今日のデータ", - thisWeek: "今週のデータ", - thisMonth: "今月のデータ" + today: '今日のデータ', + thisWeek: '今週のデータ', + thisMonth: '今月のデータ', }, mergeHostLabel: 'URLをマージ', - viewMore: chromeBase.ja.allFunction, fileName: 'オンライン時間_{today}_by_{app}', saveAsImageTitle: 'ダウンロード', restoreTitle: '刷新', @@ -128,11 +102,8 @@ const _default: Messages = { updateVersion: '更新', updateVersionInfo: '最新バージョン:{version}', updateVersionInfo4Firefox: '管理ページで {version} にアップグレードしてください', - currentVersion: chromeBase.ja.currentVersion, - appName: chromeBase.ja.name, - item: itemMessages.ja, - timeDuration: popupDurationMessages.ja - } + }, } -export default _default + +export default _default \ No newline at end of file diff --git a/src/i18n/message/popup/index.ts b/src/i18n/message/popup/index.ts new file mode 100644 index 000000000..47cefa183 --- /dev/null +++ b/src/i18n/message/popup/index.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import baseMessages, { BaseMessage } from "../common/base" +import itemMessages, { ItemMessage } from "../common/item" +import metaMessages, { MetaMessage } from "../common/meta" +import popupDurationMessages, { PopupDurationMessage } from "../common/popup-duration" +import chartMessages, { ChartMessage } from "./chart" + +export type PopupMessage = { + chart: ChartMessage + duration: PopupDurationMessage + item: ItemMessage + meta: MetaMessage + base: BaseMessage +} + +const _default: Messages = { + zh_CN: { + chart: chartMessages.zh_CN, + duration: popupDurationMessages.zh_CN, + item: itemMessages.zh_CN, + meta: metaMessages.zh_CN, + base: baseMessages.zh_CN, + }, + zh_TW: { + chart: chartMessages.zh_TW, + duration: popupDurationMessages.zh_TW, + item: itemMessages.zh_TW, + meta: metaMessages.zh_TW, + base: baseMessages.zh_TW, + }, + en: { + chart: chartMessages.en, + duration: popupDurationMessages.en, + item: itemMessages.en, + meta: metaMessages.en, + base: baseMessages.en, + }, + ja: { + chart: chartMessages.ja, + duration: popupDurationMessages.ja, + item: itemMessages.ja, + meta: metaMessages.ja, + base: baseMessages.ja, + }, +} + +export default _default \ No newline at end of file diff --git a/src/package.ts b/src/package.ts index 627eacbfc..8fcfceb90 100644 --- a/src/package.ts +++ b/src/package.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import packageJson from '../package.json' // The declaration of package.json diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index c1d2497a9..2864a865a 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -49,9 +49,9 @@ const legend2LabelStyle = (legend: string) => { function calculateAverageText(type: timer.stat.Dimension, averageValue: number): string | undefined { if (type === 'focus') { - return t(msg => msg.averageTime, { value: formatPeriodCommon(parseInt(averageValue.toFixed(0))) }) + return t(msg => msg.chart.averageTime, { value: formatPeriodCommon(parseInt(averageValue.toFixed(0))) }) } else if (type === 'time') { - return t(msg => msg.averageCount, { value: averageValue.toFixed(1) }) + return t(msg => msg.chart.averageCount, { value: averageValue.toFixed(1) }) } return undefined } @@ -150,7 +150,7 @@ function calculateSubTitleText(date: Date | Date[]) { export function pieOptions(props: timer.popup.ChartProps, container: HTMLDivElement): EcOption { const { type, data, displaySiteName, chartTitle, date } = props const titleText = chartTitle - const subTitleText = `${calculateSubTitleText(date)} @ ${t(msg => msg.appName)}` + const subTitleText = `${calculateSubTitleText(date)} @ ${t(msg => msg.meta.name)}` const textColor = getPrimaryTextColor() const secondaryColor = getSecondaryTextColor() const options: EcOption = { @@ -186,14 +186,14 @@ export function pieOptions(props: timer.popup.ChartProps, container: HTMLDivElem feature: { restore: { show: true, - title: t(msg => msg.restoreTitle) + title: t(msg => msg.chart.restoreTitle) }, saveAsImage: { show: true, - title: t(msg => msg.saveAsImageTitle), + title: t(msg => msg.chart.saveAsImageTitle), // file name - name: t(msg => msg.fileName, { - app: t(msg => msg.appName), + name: t(msg => msg.chart.fileName, { + app: t(msg => msg.meta.name), today }), excludeComponents: ['toolbox'], @@ -202,7 +202,7 @@ export function pieOptions(props: timer.popup.ChartProps, container: HTMLDivElem // Customized tool's name must start with 'my' myOptions: { show: true, - title: t(msg => msg.options), + title: t(msg => msg.chart.options), icon: optionIcon, onclick() { chrome.tabs.create({ url: getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' }) }) diff --git a/src/popup/components/footer/all-function.ts b/src/popup/components/footer/all-function.ts index b88605613..7907de11d 100644 --- a/src/popup/components/footer/all-function.ts +++ b/src/popup/components/footer/all-function.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import { getAppPageUrl } from "@util/constant/url" import { t } from "@popup/locale" @@ -12,7 +13,7 @@ function initAllFunction() { allFunctionLink.onclick = async () => { chrome.tabs.create({ url: getAppPageUrl(false, '/') }) } - allFunctionLink.innerText = t(msg => msg.viewMore) + allFunctionLink.innerText = t(msg => msg.base.allFunction) } export default initAllFunction \ No newline at end of file diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index f0fff6873..1cbe6c8a0 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -16,7 +16,7 @@ import TypeSelectWrapper from "./select/type-select" import timerService, { SortDirect } from "@service/timer-service" import { t } from "@popup/locale" // Import from i18n -import { locale } from "@util/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" @@ -96,7 +96,7 @@ class FooterWrapper { const rows = await timerService.select(queryParam, FILL_FLAG_PARAM) const popupRows: timer.popup.Row[] = [] const other: timer.popup.Row = { - host: t(msg => msg.otherLabel, { count: 0 }), + host: t(msg => msg.chart.otherLabel, { count: 0 }), focus: 0, total: 0, date: '0000-00-00', @@ -115,7 +115,7 @@ class FooterWrapper { otherCount++ } } - other.host = t(msg => msg.otherLabel, { count: otherCount }) + other.host = t(msg => msg.chart.otherLabel, { count: otherCount }) popupRows.push(other) const type = queryParam.sort as timer.stat.Dimension const data = popupRows.filter(item => item[type]) @@ -140,7 +140,7 @@ class FooterWrapper { mergeHost: this.mergeHostWrapper.mergedHost(), sort: this.typeSelectWrapper.getSelectedType(), sortOrder: SortDirect.DESC, - chartTitle: t(msg => msg.title[duration]), + chartTitle: t(msg => msg.chart.title[duration]), mergeDate: true, } return param diff --git a/src/popup/components/footer/merge-host.ts b/src/popup/components/footer/merge-host.ts index ba26ee3fe..2d27a616f 100644 --- a/src/popup/components/footer/merge-host.ts +++ b/src/popup/components/footer/merge-host.ts @@ -23,7 +23,7 @@ class MergeHostWrapper { this.mergeHostSwitch = document.getElementById('merge-host-switch') this.mergeHostPopup = document.getElementById('merge-host-popup-container') this.mergeHostPopupInfo = document.getElementById('merge-host-popup-info') - this.mergeHostPopupInfo.innerText = t(msg => msg.mergeHostLabel) + this.mergeHostPopupInfo.innerText = t(msg => msg.chart.mergeHostLabel) // Handle hover this.mergeHostSwitch.onmouseover = () => this.mergeHostPopup.style.display = 'block' this.mergeHostSwitch.onmouseout = () => this.mergeHostPopup.style.display = 'none' diff --git a/src/popup/components/footer/select/time-select.ts b/src/popup/components/footer/select/time-select.ts index 15882d444..8cbe74554 100644 --- a/src/popup/components/footer/select/time-select.ts +++ b/src/popup/components/footer/select/time-select.ts @@ -41,7 +41,7 @@ class TimeSelectWrapper { private initOption(item: timer.popup.Duration) { const li = document.createElement('li') li.classList.add('el-select-dropdown__item') - li.innerText = t(msg => msg.timeDuration[item]) + li.innerText = t(msg => msg.duration[item]) li.onclick = () => { this.selected(item) this.handleSelected?.() @@ -55,7 +55,7 @@ class TimeSelectWrapper { this.currentSelected = item Array.from(this.optionItems.values()).forEach(item => item.classList.remove(SELECTED_CLASS)) this.optionItems.get(item).classList.add(SELECTED_CLASS) - this.timeSelectInput.value = t(msg => msg.timeDuration[item]) + this.timeSelectInput.value = t(msg => msg.duration[item]) } private openPopup() { diff --git a/src/popup/components/footer/total-info.ts b/src/popup/components/footer/total-info.ts index e799e634b..692a2363d 100644 --- a/src/popup/components/footer/total-info.ts +++ b/src/popup/components/footer/total-info.ts @@ -18,10 +18,10 @@ import { sum } from "@util/array" function getTotalInfo(data: timer.stat.Row[], type: timer.stat.Dimension): string { if (type === 'time') { const totalCount = sum(data.map(d => d.time || 0)) - return t(msg => msg.totalCount, { totalCount }) + return t(msg => msg.chart.totalCount, { totalCount }) } else if (type === 'focus') { const totalTime = formatPeriodCommon(sum(data.map(d => d.focus || 0))) - return t(msg => msg.totalTime, { totalTime }) + return t(msg => msg.chart.totalTime, { totalTime }) } else { return '' } diff --git a/src/popup/components/footer/upgrade.ts b/src/popup/components/footer/upgrade.ts index d63aa52be..e2a4d5605 100644 --- a/src/popup/components/footer/upgrade.ts +++ b/src/popup/components/footer/upgrade.ts @@ -49,16 +49,16 @@ function showUpgradeButton(latestVersion: string) { upgrade.onmouseover = () => upgradePopup.style.display = 'block' upgrade.onmouseout = () => upgradePopup.style.display = 'none' - upgradeLink.innerText = t(msg => msg.updateVersion) + upgradeLink.innerText = t(msg => msg.chart.updateVersion) const versionLabel = `v${latestVersion}` if (IS_FIREFOX) { // Can't jump to about:addons in Firefox // So no jump, only show tooltip text upgrade.classList.add("firefox-upgrade-no-underline") - latestInfo.innerText = t(msg => msg.updateVersionInfo4Firefox, { version: versionLabel }) + latestInfo.innerText = t(msg => msg.chart.updateVersionInfo4Firefox, { version: versionLabel }) } else { upgradeLink.onclick = () => chrome.tabs.create({ url: UPDATE_PAGE }) - latestInfo.innerText = t(msg => msg.updateVersionInfo, { version: versionLabel }) + latestInfo.innerText = t(msg => msg.chart.updateVersionInfo, { version: versionLabel }) } } diff --git a/src/popup/index.ts b/src/popup/index.ts index 0602160f3..2de7d45b7 100644 --- a/src/popup/index.ts +++ b/src/popup/index.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + import "./style" import renderChart, { handleRestore } from "./components/chart" @@ -13,7 +14,7 @@ import "../common/timer" import { toggle, init as initTheme } from "@util/dark-mode" import optionService from "@service/option-service" -import { initLocale } from "@util/i18n" +import { initLocale } from "@i18n" async function main() { await initLocale() diff --git a/src/popup/locale/index.ts b/src/popup/locale.ts similarity index 61% rename from src/popup/locale/index.ts rename to src/popup/locale.ts index 8c00aa7c9..3f3a5f15d 100644 --- a/src/popup/locale/index.ts +++ b/src/popup/locale.ts @@ -5,12 +5,12 @@ * https://opensource.org/licenses/MIT */ -import { I18nKey as _I18nKey, locale, t as _t } from "@util/i18n" -import messages, { PopupMessage } from "./messages" +import { I18nKey as _I18nKey, t as _t } from "@i18n" +import messages, { PopupMessage } from "@i18n/message/popup" export type I18nKey = _I18nKey export const t = (key: I18nKey, param?: any) => { const props = { key, param } - return _t(messages[locale], props) + return _t(messages, props) } diff --git a/src/popup/style/index.sass b/src/popup/style/index.sass index deb4a5ac4..7f1242636 100644 --- a/src/popup/style/index.sass +++ b/src/popup/style/index.sass @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + @import url("element-plus/theme-chalk/base.css") @import url("element-plus/theme-chalk/el-select.css") @import url("element-plus/theme-chalk/el-input.css") diff --git a/src/util/array.ts b/src/util/array.ts index a6591b1f7..431ed350e 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + /** * Group by * diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 1a38352c0..2f2c8036d 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -123,5 +123,20 @@ export const PSL_HOMEPAGE = 'https://publicsuffix.org/' /** * @since 0.9.3 + * @deprecated 1.4.0 Use crowdin to manage translations */ export const TRANSLATION_ISSUE_PAGE = 'https://docs.google.com/forms/d/e/1FAIpQLSdZSmEZp6Xfmb5v-3H4hsubgeCReDayDOuWDWWU5C1W80exGA/viewform?usp=sf_link' + +/** + * The id of project on crowdin.com + * + * @since 1.4.0 + */ +export const CROWDIN_PROJECT_ID = 516822 + +/** + * The url of project on crowdin.com + * + * @since 1.4.0 + */ +export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' \ No newline at end of file diff --git a/src/util/fifo-cache.ts b/src/util/fifo-cache.ts index 8649fa69c..131e01ad3 100644 --- a/src/util/fifo-cache.ts +++ b/src/util/fifo-cache.ts @@ -4,6 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ + const DEFAULT_THRESHOLD = 10 /** diff --git a/src/util/i18n/components/locale.ts b/src/util/i18n/components/locale.ts deleted file mode 100644 index 7d17f23a9..000000000 --- a/src/util/i18n/components/locale.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { Messages } from ".." - -/** - * Meta info of locales - * - * @since 0.8.0 - */ -export type LocaleMessage = { - name: string -} - -const _default: Messages = { - zh_CN: { - name: "简体中文", - }, - zh_TW: { - name: "正體中文", - }, - en: { - name: "English" - }, - ja: { - name: "日本語" - } -} - -export default _default \ No newline at end of file diff --git a/src/util/i18n/components/popup-duration.ts b/src/util/i18n/components/popup-duration.ts deleted file mode 100644 index 9cc8b8e88..000000000 --- a/src/util/i18n/components/popup-duration.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { Messages } from ".." - -export type PopupDurationMessage = { [key in timer.popup.Duration]: string } - -const _default: Messages = { - zh_CN: { - today: "今日", - thisWeek: "本周", - thisMonth: "本月" - }, - zh_TW: { - today: "今日", - thisWeek: "本週", - thisMonth: "本月" - }, - en: { - today: "Today's", - thisWeek: "This Week's", - thisMonth: "This Month's" - }, - ja: { - today: "今日の", - thisWeek: "今週の", - thisMonth: "今月の" - } -} - -export default _default \ No newline at end of file diff --git a/test/util/chrome/compile.test.ts b/test/util/chrome/compile.test.ts index 4da42f9d2..cce0939b3 100644 --- a/test/util/chrome/compile.test.ts +++ b/test/util/chrome/compile.test.ts @@ -1,4 +1,4 @@ -import compile from "@util/i18n/chrome/compile" +import compile from "@i18n/chrome/compile" test('1', () => { const messages = { app: '123', foo: { bar: '234' } } diff --git a/tsconfig.json b/tsconfig.json index af9f1dbc0..a77db8a72 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,12 @@ "@util/*": [ "src/util/*" ], + "@i18n/*": [ + "src/i18n/*" + ], + "@i18n": [ + "src/i18n/index" + ], "@echarts/*": [ "src/echarts/*" ], diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 8c9bda3ca..cc8af3f96 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -4,7 +4,7 @@ import CopyWebpackPlugin from "copy-webpack-plugin" import webpack from "webpack" // Generate json files import manifest from "../src/manifest" -import i18nChrome from "../src/util/i18n/chrome" +import i18nChrome from "../src/i18n/chrome" import tsConfig from '../tsconfig.json' import MiniCssExtractPlugin from "mini-css-extract-plugin" const tsPathAlias = tsConfig.compilerOptions.paths @@ -59,7 +59,7 @@ const staticOptions: webpack.Configuration = { rules: [ { test: /\.ts$/, - exclude: /^(node_modules|test)/, + exclude: /^(node_modules|test|script)/, use: ['ts-loader'] }, { test: /\.css$/, From b811e3987c2c8f661462d235c8d5fe04b18f571d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 20 Dec 2022 21:56:32 +0800 Subject: [PATCH 059/168] Fix error in non-html page (#174) --- src/content-script/limit.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index baa8190e6..735b48d02 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -52,6 +52,9 @@ class _Modal { } showModal(showDelay: boolean) { + if (!document.body) { + return + } const _thisUrl = this.url if (showDelay && this.mask.childElementCount === 1) { this.delayContainer = document.createElement('p') @@ -84,7 +87,7 @@ class _Modal { } hideModal() { - if (!this.visible) { + if (!this.visible || document.body) { return } this.mask.remove() From b40c098c8c6dbc8d277e2d41815ea88ad41e987d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 20 Dec 2022 22:15:27 +0800 Subject: [PATCH 060/168] Support more locales --- global.d.ts | 6 ++++-- src/app/components/help-us/progress-list.ts | 18 ++++++++++++++---- src/i18n/message/common/locale.ts | 6 ++++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/global.d.ts b/global.d.ts index 87834cf98..9a1fcdcab 100644 --- a/global.d.ts +++ b/global.d.ts @@ -191,14 +191,16 @@ declare namespace timer { */ type TranslatingLocale = | 'de' - | 'en_GB' - | 'en_US' | 'es' | 'ko' | 'pl' | 'pt' | 'pt_BR' | 'ru' + | 'uk' + | 'fr' + | 'it' + | 'sv' namespace stat { /** diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index 1e58786d7..0e820c9b7 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -19,14 +19,16 @@ const localeCrowdMap: { [locale in SupportedLocale]: string } = { ja: "ja", zh_TW: "zh-TW", de: "de", - en_GB: "en-GB", - en_US: "en-US", es: "es-ES", ko: "ko", pl: "pl", pt: "pt-PT", pt_BR: "pt-BR", - ru: "ru" + ru: "ru", + uk: "uk", + fr: "fr", + it: "it", + sv: "sv-SE", } const crowdLocaleMap: { [locale: string]: SupportedLocale } = {} @@ -69,7 +71,15 @@ const CONTAINER_CLZ = 'progress-container' async function queryData(listRef: Ref) { const loading = ElLoading.service({ target: `.${CONTAINER_CLZ}`, text: t(msg => msg.helpUs.loading) }) const langList = await getTranslationStatus() - listRef.value = langList.map(convert2Info).sort((a, b) => b.progress - a.progress) + listRef.value = langList.map(convert2Info) + .sort((a, b) => { + const progressDiff = b.progress - a.progress + if (progressDiff === 0) { + return a.locale.localeCompare(b.locale) + } else { + return progressDiff + } + }) loading.close() } diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index 467ecb5e5..b48008dc7 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -19,8 +19,6 @@ const _default: LocaleMessages = { zh_TW: '正體中文', en: 'English', ja: '日本語', - en_GB: 'English, UK', - en_US: 'English, US', pl: 'Polski', pt: 'Português', pt_BR: 'Portugues, Brasil', @@ -28,6 +26,10 @@ const _default: LocaleMessages = { de: 'Deutsch', es: 'Español', ru: 'Русский', + uk: "українська", + fr: "Français", + it: "italiano", + sv: "Sverige", } export default _default \ No newline at end of file From 7a196c2128fce4fe2c8f0ee9503a6f90b38d6964 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 20 Dec 2022 22:40:30 +0800 Subject: [PATCH 061/168] Fix i18n of chrome --- src/manifest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/manifest.ts b/src/manifest.ts index c39e6fe8a..5a47abfd0 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -16,8 +16,8 @@ import packageInfo from "./package" import { OPTION_ROUTE } from "./app/router/constants" const { version, author, homepage } = packageInfo const _default: chrome.runtime.ManifestV2 = { - name: '__MSG_app_marketName__', - description: "__MSG_app_description__", + name: '__MSG_meta_marketName__', + description: "__MSG_meta_description__", version, author, default_locale: 'en', From d7cce47c86f1b7c15462fa8c41dbd7c34a595332 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 20 Dec 2022 22:40:38 +0800 Subject: [PATCH 062/168] v1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7ccd66b1..912720fd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.3.4", + "version": "1.4.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From 8a1c59c446fdd79be31659b8ea8d0813e71543e4 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 20 Dec 2022 22:49:24 +0800 Subject: [PATCH 063/168] v1.4.1.firefox --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 912720fd9..00e5917e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.0", + "version": "1.4.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From bb1c850c7ae29cadadb244310e881c6edea74839 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 22 Dec 2022 18:19:02 +0800 Subject: [PATCH 064/168] Upgrade dependencies --- package.json | 46 +++++++++++++++++++-------------------- src/api/gist.ts | 49 +++++++++++++++--------------------------- src/util/fifo-cache.ts | 4 +++- src/util/pattern.ts | 2 +- 4 files changed, 44 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 00e5917e8..17d51410d 100644 --- a/package.json +++ b/package.json @@ -16,48 +16,48 @@ "license": "MIT", "devDependencies": { "@crowdin/crowdin-api-client": "^1.19.2", - "@types/chrome": "0.0.199", + "@types/chrome": "0.0.205", "@types/copy-webpack-plugin": "^8.0.1", "@types/echarts": "^4.9.16", "@types/generate-json-webpack-plugin": "^0.3.4", - "@types/jest": "^29.2.0", - "@types/node": "^18.11.3", + "@types/jest": "^29.2.4", + "@types/node": "^18.11.17", "@types/psl": "^1.1.0", "@types/webpack": "^5.28.0", "@types/webpack-bundle-analyzer": "^4.6.0", - "babel-loader": "^8.2.5", + "babel-loader": "^9.1.0", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.1", - "eslint": "^8.26.0", - "filemanager-webpack-plugin": "^7.0.0", + "css-loader": "^6.7.3", + "eslint": "^8.30.0", + "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", - "jest": "^29.2.1", - "jest-environment-jsdom": "^29.2.1", - "mini-css-extract-plugin": "^2.6.1", - "node-sass": "^7.0.3", - "sass-loader": "^13.1.0", + "jest": "^29.3.1", + "jest-environment-jsdom": "^29.3.1", + "mini-css-extract-plugin": "^2.7.2", + "node-sass": "^8.0.0", + "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "ts-jest": "^29.0.3", - "ts-loader": "^9.4.1", + "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.1", - "tslib": "^2.4.0", - "typescript": "4.8.4", + "tslib": "^2.4.1", + "typescript": "4.9.4", "url-loader": "^4.1.1", - "webpack": "^5.74.0", - "webpack-bundle-analyzer": "^4.6.1", - "webpack-cli": "^4.10.0" + "webpack": "^5.75.0", + "webpack-bundle-analyzer": "^4.7.0", + "webpack-cli": "^5.0.1" }, "dependencies": { "@element-plus/icons-vue": "^2.0.10", - "axios": "^1.1.3", + "axios": "^1.2.1", "clipboardy": "^3.0.0", "countup.js": "^2.3.2", - "echarts": "^5.4.0", - "element-plus": "2.2.19", + "echarts": "^5.4.1", + "element-plus": "2.2.27", "psl": "^1.9.0", "stream-browserify": "^3.0.0", - "vue": "^3.2.41", - "vue-router": "^4.1.5" + "vue": "^3.2.45", + "vue-router": "^4.1.6" } } \ No newline at end of file diff --git a/src/api/gist.ts b/src/api/gist.ts index decc6de48..06b1b43c7 100644 --- a/src/api/gist.ts +++ b/src/api/gist.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import type { AxiosAdapter, AxiosError, AxiosResponse } from "axios" +import type { AxiosError, AxiosResponse } from "axios" import FIFOCache from "@util/fifo-cache" @@ -45,38 +45,23 @@ const BASE_URL = 'https://api.github.com/gists' */ const GET_CACHE = new FIFOCache(20) -function createCacheAdaptor(originAdaptor: AxiosAdapter): AxiosAdapter { - return (config) => { - const { url, method } = config; - let useCache = method === 'get' && url.startsWith(BASE_URL) - if (useCache) { - // Use url and token as key - const key = url + config?.headers?.['Authorization'] - return GET_CACHE.getOrSupply(key, () => originAdaptor(config)) - } else { - return originAdaptor(config) - } - } -} - async function get(token: string, uri: string): Promise { - return new Promise(resolve => axios.get(BASE_URL + uri, { - headers: { - "Accept": "application/vnd.github+json", - "Authorization": `token ${token}` - }, - // Use cache - adapter: createCacheAdaptor(axios.defaults.adapter) - }).then(response => { - if (response.status >= 200 && response.status < 300) { - return resolve(response.data as T) - } else { - return resolve(null) - } - }).catch((error: AxiosError) => { - console.log("AxisError", error) - resolve(null) - })) + const headers = { + "Accept": "application/vnd.github+json", + "Authorization": `token ${token}` + } + const cacheKey = uri + token + return GET_CACHE.getOrSupply(cacheKey, () => axios.get(BASE_URL + uri, { headers })) + .then(response => { + if (response.status >= 200 && response.status < 300) { + return response.data as T + } else { + return null + } + }).catch((error: AxiosError) => { + console.log("AxisError", error) + return null + }) } async function post(token: string, uri: string, body?: R): Promise { diff --git a/src/util/fifo-cache.ts b/src/util/fifo-cache.ts index 131e01ad3..fdcd959e6 100644 --- a/src/util/fifo-cache.ts +++ b/src/util/fifo-cache.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import { log } from "@src/common/logger" + const DEFAULT_THRESHOLD = 10 /** @@ -44,7 +46,7 @@ class FIFOCache { async getOrSupply(key: string, supplier: () => PromiseLike): Promise { const exist = this.map[key] if (exist) { - console.log("Hit cache with key: " + key) + log("Hit cache with key: " + key) return exist } const value = await supplier() diff --git a/src/util/pattern.ts b/src/util/pattern.ts index 29282b424..1a8953ef3 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -23,7 +23,7 @@ export function isBrowserUrl(url: string) { const isNotValidPort = (portStr: string) => { const port = parseInt(portStr) - return port === NaN || port < 0 || port > 65535 || port.toString() !== portStr + return port < 0 || port > 65535 || port.toString() !== portStr } /** From 61c0c60bc773ec454a2dffba10c93961dc91b3df Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 22 Dec 2022 18:39:11 +0800 Subject: [PATCH 065/168] Canvas -> SVG --- src/app/components/dashboard/components/calendar-heat-map.ts | 4 ++-- src/app/components/habit/component/chart/wrapper.ts | 4 ++-- src/app/components/trend/components/chart/wrapper.ts | 4 ++-- src/echarts/{canvas-renderer.ts => svg-renderer.ts} | 4 ++-- src/popup/components/chart/index.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename src/echarts/{canvas-renderer.ts => svg-renderer.ts} (54%) diff --git a/src/app/components/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 00a2ca8f3..420364967 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -15,11 +15,11 @@ import GridComponent from "@echarts/component/grid" import VisualMapComponent from "@echarts/component/visual-map" import HeatmapChart from "@echarts/chart/heatmap" import { init, use } from "@echarts/core" -import CanvasRenderer from "@echarts/canvas-renderer" +import SVGRenderer from "@echarts/svg-renderer" // Register echarts use([ - CanvasRenderer, + SVGRenderer, HeatmapChart, TooltipComponent, GridComponent, diff --git a/src/app/components/habit/component/chart/wrapper.ts b/src/app/components/habit/component/chart/wrapper.ts index 7486fe823..13b212a93 100644 --- a/src/app/components/habit/component/chart/wrapper.ts +++ b/src/app/components/habit/component/chart/wrapper.ts @@ -12,13 +12,13 @@ import type { GridComponentOption, TitleComponentOption, ToolboxComponentOption, import { use, init } from "@echarts/core" import BarChart from "@echarts/chart/bar" -import CanvasRenderer from "@echarts/canvas-renderer" +import SVGRenderer from "@echarts/svg-renderer" import TitleComponent from "@echarts/component/title" import TooltipComponent from "@echarts/component/tooltip" import ToolboxComponent from "@echarts/component/toolbox" import GridComponent from "@echarts/component/grid" -use([BarChart, CanvasRenderer, TitleComponent, TooltipComponent, ToolboxComponent, GridComponent]) +use([BarChart, SVGRenderer, TitleComponent, TooltipComponent, ToolboxComponent, GridComponent]) import { formatPeriodCommon, formatTime, MILL_PER_DAY } from "@util/time" import { t } from "@app/locale" diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts index d059e1596..78de978bd 100644 --- a/src/app/components/trend/components/chart/wrapper.ts +++ b/src/app/components/trend/components/chart/wrapper.ts @@ -19,7 +19,7 @@ import type { import { init, use } from "@echarts/core" import LineChart from "@echarts/chart/line" import GridComponent from "@echarts/component/grid" -import CanvasRenderer from "@echarts/canvas-renderer" +import SVGRenderer from "@echarts/svg-renderer" import LegendComponent from "@echarts/component/legend" import TitleComponent from "@echarts/component/title" import ToolboxComponent from "@echarts/component/toolbox" @@ -38,7 +38,7 @@ use([ TitleComponent, ToolboxComponent, TooltipComponent, - CanvasRenderer, + SVGRenderer, ]) type EcOption = ComposeOption< diff --git a/src/echarts/canvas-renderer.ts b/src/echarts/svg-renderer.ts similarity index 54% rename from src/echarts/canvas-renderer.ts rename to src/echarts/svg-renderer.ts index a71cb5c66..7671545a4 100644 --- a/src/echarts/canvas-renderer.ts +++ b/src/echarts/svg-renderer.ts @@ -5,6 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { install as CanvasRenderer } from "echarts/lib/renderer/installCanvasRenderer" +import { install as SVGRenderer } from "echarts/lib/renderer/installSVGRenderer" -export default CanvasRenderer \ No newline at end of file +export default SVGRenderer \ No newline at end of file diff --git a/src/popup/components/chart/index.ts b/src/popup/components/chart/index.ts index 883161792..746bb1e4a 100644 --- a/src/popup/components/chart/index.ts +++ b/src/popup/components/chart/index.ts @@ -14,10 +14,10 @@ import TitleComponent from "@echarts/component/title" import ToolboxComponent from "@echarts/component/toolbox" import TooltipComponent from "@echarts/component/tooltip" import LegendComponent from "@echarts/component/legend" -import CanvasRenderer from "@echarts/canvas-renderer" +import SVGRenderer from "@echarts/svg-renderer" // Register echarts -use([TitleComponent, ToolboxComponent, TooltipComponent, LegendComponent, CanvasRenderer, PieChart]) +use([TitleComponent, ToolboxComponent, TooltipComponent, LegendComponent, SVGRenderer, PieChart]) import { defaultStatistics } from "@util/constant/option" import OptionDatabase from "@db/option-database" From 00860b9e53467cac63288e28cd3c5bca99876683 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 23 Dec 2022 16:46:46 +0800 Subject: [PATCH 066/168] Optimize user manual(#175) --- src/app/layout/menu.ts | 9 +++++++-- src/guide/component/usage.ts | 13 +++++++++++++ src/guide/layout/menu.ts | 5 ++++- src/i18n/message/app/menu.ts | 5 +++++ src/i18n/message/guide/layout.ts | 5 +++++ src/i18n/message/guide/usage.ts | 33 ++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 994c52013..b6cf29d8b 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -15,8 +15,8 @@ import { defineComponent, h, onMounted, reactive } from "vue" import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" -import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE } from "@util/constant/url" -import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Rank, SetUp, Stopwatch, Sugar, Tickets, Timer } from "@element-plus/icons-vue" +import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/constant/url" +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" @@ -43,6 +43,11 @@ type _RouteProps = { */ function generateMenus(): _MenuGroup[] { const otherMenuItems: _MenuItem[] = [{ + title: 'userManual', + href: getGuidePageUrl(false), + icon: Memo, + index: '_guide', + }, { title: 'helpUs', route: '/other/help', icon: HelpFilled, diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts index 4308dbfad..0b0748c35 100644 --- a/src/guide/component/usage.ts +++ b/src/guide/component/usage.ts @@ -45,6 +45,18 @@ const advanced = () => [ ), ] +const backup = () => [ + h2(msg => msg.layout.menu.usage.backup, 'backup'), + paragraph(msg => msg.usage.backup.p1, { gist: link('https://gist.github.com/', 'Github Gist') }), + list( + [msg => msg.usage.backup.l1, { + token: link('https://github.com/settings/tokens', 'token') + }], + msg => msg.usage.backup.l2, + msg => msg.usage.backup.l3, + ), +] + const _default = defineComponent({ setup() { return () => section( @@ -52,6 +64,7 @@ const _default = defineComponent({ ...quickstart(), ...background(), ...advanced(), + ...backup(), ) } }) diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 025e816b4..7fea3c936 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -42,7 +42,10 @@ const menus: _Group[] = [ }, { title: msg => msg.layout.menu.usage.advanced, position: 'advanced' - }, + }, { + title: msg => msg.layout.menu.usage.backup, + position: 'backup', + } ], icon: Memo }, diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index de8bac01d..ccf04f374 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -23,6 +23,7 @@ export type MenuMessage = { feedback: string rate: string helpUs: string + userManual: string } const _default: Messages = { @@ -44,6 +45,7 @@ const _default: Messages = { feedback: '有什么反馈吗?', rate: '打个分吧!', helpUs: '帮助我们~', + userManual: '用户手册', }, zh_TW: { dashboard: '儀錶盤', @@ -63,6 +65,7 @@ const _default: Messages = { feedback: '有什麼反饋嗎?', rate: '打個分吧!', helpUs: '帮助我们~', + userManual: '用戶手冊', }, en: { dashboard: 'Dashboard', @@ -82,6 +85,7 @@ const _default: Messages = { feedback: 'Feedback Questionnaire', rate: 'Rate It', helpUs: 'Help Us', + userManual: 'User Manual', }, ja: { dashboard: 'ダッシュボード', @@ -101,6 +105,7 @@ const _default: Messages = { feedback: 'フィードバックアンケート', rate: 'それを評価', helpUs: 'Help Us', + userManual: 'ユーザーマニュアル', } } diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 5552e96eb..66f7982c4 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -13,6 +13,7 @@ export type LayoutMessage = { quickstart: string background: string advanced: string + backup: string } privacy: { title: string @@ -31,6 +32,7 @@ const _default: Messages = { quickstart: '快速开始', background: '访问后台页面', advanced: '高级功能', + backup: '使用 Gist 备份数据', }, privacy: { title: '隐私声明', @@ -47,6 +49,7 @@ const _default: Messages = { quickstart: '快速開始', background: '訪問後台頁面', advanced: '高級功能', + backup: '使用 Gist 備份數據', }, privacy: { title: '隱私聲明', @@ -63,6 +66,7 @@ const _default: Messages = { quickstart: 'Quickstart', background: 'Using all functions', advanced: 'Advanced features', + backup: 'Backup your data with Gist', }, privacy: { title: 'Privary Policy', @@ -79,6 +83,7 @@ const _default: Messages = { quickstart: 'クイックスタート', background: 'すべての機能', advanced: '高度な機能', + backup: 'Backup your data with Gist', }, privacy: { title: 'ポリシーと規約', diff --git a/src/i18n/message/guide/usage.ts b/src/i18n/message/guide/usage.ts index 6d47d9e49..4b048c8c5 100644 --- a/src/i18n/message/guide/usage.ts +++ b/src/i18n/message/guide/usage.ts @@ -30,10 +30,17 @@ type _AdvancedKey = | 'l7' | 'l8' +type _BackupKey = + | 'p1' + | 'l1' + | 'l2' + | 'l3' + export type UsageMessage = { quickstart: { [key in _QuickstartKey]: string } background: { [key in _BackgroundKey]: string } advanced: { [key in _AdvancedKey]: string } + backup: { [key in _BackupKey]: string } } const _default: Messages = { @@ -63,6 +70,12 @@ const _default: Messages = { l7: '7. 它支持夜间模式,同样需要在选项里启用。', l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。', }, + backup: { + p1: '您可以按以下步骤使用 {gist} 备份您的数据。之后,您可在其他终端上查询已备份数据。', + l1: '1. 首先,您需要在 Github 生成一个包含 gist 权限的 {token}。', + l2: '2. 然后在选项页面将同步方式选为 Github Gist,将你的 token 填入下方出现的输入框中。', + l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。' + }, }, zh_TW: { quickstart: { @@ -90,6 +103,12 @@ const _default: Messages = { l7: '7. 它支持夜間模式,同樣需要在選項裡啟用。', l8: '8. 它支持使用 Github Gist 作為雲端存儲多個瀏覽器的數據,並進行聚合查詢。需要您準備一個至少包含 gist 權限的 token。', }, + backup: { + p1: '您可以按以下步驟使用 {gist} 備份您的數據。之後,您可在其他終端上查詢已備份數據。', + l1: '1. 首先,您需要在 Github 生成一個包含 gist 權限的 {token}。', + l2: '2. 然後在選項頁面將同步方式選為 Github Gist,將你的 token 填入下方出現的輸入框中。', + l3: '3. 最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。' + }, }, en: { quickstart: { @@ -117,6 +136,14 @@ const _default: Messages = { l7: '7. It supports night mode, which also needs to be enabled in the options.', 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.', + 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.' + }, }, ja: { quickstart: { @@ -144,6 +171,12 @@ const _default: Messages = { l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', }, + backup: { + p1: '以下の手順に従って、{gist} を使用してデータをバックアップできます。その後、バックアップされたデータを他の端末で照会できます。', + l1: '1. まず、Github で Gist 権限を持つ {token} を生成する必要があります。', + l2: '2. 次に、オプション ページで同期方法として [Github Gist] を選択し、下に表示される入力ボックスにトークンを入力します。', + l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。' + }, }, } From f44452e74af2c409254f30ffcbe2ae43431dbab2 Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 27 Dec 2022 09:54:55 +0000 Subject: [PATCH 067/168] Remove logger --- src/util/fifo-cache.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/util/fifo-cache.ts b/src/util/fifo-cache.ts index fdcd959e6..c1fc9548f 100644 --- a/src/util/fifo-cache.ts +++ b/src/util/fifo-cache.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { log } from "@src/common/logger" - const DEFAULT_THRESHOLD = 10 /** @@ -46,7 +44,6 @@ class FIFOCache { async getOrSupply(key: string, supplier: () => PromiseLike): Promise { const exist = this.map[key] if (exist) { - log("Hit cache with key: " + key) return exist } const value = await supplier() From 833501eb8b338fbd662b99ef07de9e8364cebbb6 Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 4 Jan 2023 07:44:47 +0000 Subject: [PATCH 068/168] Set the detault value of countWhenIdlecountWhenIdle 'true' --- src/app/components/option/components/statistics.ts | 2 +- src/util/constant/option.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/option/components/statistics.ts b/src/app/components/option/components/statistics.ts index f9c901dce..741f59dc0 100644 --- a/src/app/components/option/components/statistics.ts +++ b/src/app/components/option/components/statistics.ts @@ -50,7 +50,7 @@ function renderOptionItems(option: timer.option.StatisticsOption) { input: countWhenIdle(option), idleTime: tagText(msg => msg.option.statistics.idleTime), info: tooltip(msg => msg.option.statistics.idleTimeInfo) - }, msg => msg.statistics.countWhenIdle, t(msg => msg.option.no)), + }, msg => msg.statistics.countWhenIdle, t(msg => msg.option.yes)), h(ElDivider) ) } diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index c03805941..ce5a73373 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -40,7 +40,7 @@ export function defaultAppearance(): timer.option.AppearanceOption { export function defaultStatistics(): timer.option.StatisticsOption { return { - countWhenIdle: false, + countWhenIdle: true, collectSiteName: true, countLocalFiles: false } From e52d1f387f33a621421df128be5f045bd447e3ae Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 4 Jan 2023 07:59:16 +0000 Subject: [PATCH 069/168] Fix modal error (#177) --- src/content-script/limit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index 735b48d02..fcb282449 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -87,7 +87,7 @@ class _Modal { } hideModal() { - if (!this.visible || document.body) { + if (!this.visible || !document.body) { return } this.mask.remove() From f1169055562dbc514f94340782e17a290d5facbb Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 4 Jan 2023 10:16:06 +0000 Subject: [PATCH 070/168] Fix multiple windows error (#178) --- src/background/badge-text-manager.ts | 16 ++++++++++------ src/background/index.ts | 12 ++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 87c05dfc5..4991bd21d 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -75,22 +75,25 @@ function findActiveTab(): Promise { })) } -async function updateFocus(badgeLocation?: BadgeLocation) { +async function updateFocus(badgeLocation?: BadgeLocation, lastLocation?: BadgeLocation): Promise { + // Clear the last tab firstly + lastLocation?.tabId && setBadgeText('', lastLocation.tabId) badgeLocation = badgeLocation || await findActiveTab() if (!badgeLocation) { - return + return badgeLocation } const { url, tabId } = badgeLocation if (!url || isBrowserUrl(url)) { - return + return badgeLocation } const host = extractHostname(url)?.host if (whitelistHolder.contains(host)) { setBadgeText('W', tabId) - return + return badgeLocation } const milliseconds = host ? (await timerDb.get(host, new Date())).focus : undefined setBadgeTextOfMills(milliseconds, tabId) + return badgeLocation } const ALARM_NAME = 'timer-badge-text-manager-alarm' @@ -102,6 +105,7 @@ function createAlarm(beforeAction?: () => void) { class BadgeTextManager { isPaused: boolean + lastLocation: BadgeLocation async init() { createAlarm() @@ -135,9 +139,9 @@ class BadgeTextManager { this.forceUpdate() } - forceUpdate(badgeLocation?: BadgeLocation) { + async forceUpdate(badgeLocation?: BadgeLocation) { if (this.isPaused) { return } - updateFocus(badgeLocation) + this.lastLocation = await updateFocus(badgeLocation, this.lastLocation) } private pauseOrResumeAccordingToOption(displayBadgeText: boolean) { diff --git a/src/background/index.ts b/src/background/index.ts index 83f03e67e..cad22a406 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -19,6 +19,7 @@ import { getGuidePageUrl } from "@util/constant/url" import MessageDispatcher from "./message-dispatcher" import initLimitProcesser from "./limit-processor" import initCsHandler from "./content-script-handler" +import { isBrowserUrl } from "@util/pattern" // Open the log of console openLog() @@ -54,6 +55,17 @@ new ActiveTabListener() .register(({ url, tabId }) => badgeTextManager.forceUpdate({ url, tabId })) .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)) + .forEach(({ url, id }) => badgeTextManager.forceUpdate({ url, tabId: id })) + ) +}) + // Collect the install time chrome.runtime.onInstalled.addListener(async detail => { if (detail.reason === "install") { From b518d629fec140d8d89f4825255e16f3ec637ce8 Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 4 Jan 2023 10:16:30 +0000 Subject: [PATCH 071/168] v1.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17d51410d..4287c868a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.1", + "version": "1.4.2", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From 35b2d4a652b4511a8ac461fbf3338b34bf0595c3 Mon Sep 17 00:00:00 2001 From: ZHY Date: Thu, 5 Jan 2023 03:09:34 +0000 Subject: [PATCH 072/168] Guide page supports dark mode --- src/guide/index.ts | 6 +- src/guide/style/dark-theme.sass | 80 ++++++++++++++++++++++ src/guide/{style.sass => style/index.sass} | 14 ++-- src/guide/style/light-theme.sass | 12 ++++ src/util/dark-mode.ts | 2 +- 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/guide/style/dark-theme.sass rename src/guide/{style.sass => style/index.sass} (82%) create mode 100644 src/guide/style/light-theme.sass diff --git a/src/guide/index.ts b/src/guide/index.ts index b87af4365..3334af4c8 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -10,13 +10,15 @@ import 'element-plus/theme-chalk/index.css' import { initLocale } from "@i18n" import { t } from "./locale" -import { init as initTheme } from "@util/dark-mode" +import { init as initTheme, toggle } from "@util/dark-mode" import { createApp } from "vue" import Main from "./layout" - +import optionService from "@service/option-service" async function main() { initTheme() + // Calculate the latest mode + optionService.isDarkMode().then(toggle) await initLocale() const app = createApp(Main) diff --git a/src/guide/style/dark-theme.sass b/src/guide/style/dark-theme.sass new file mode 100644 index 000000000..38b9ef1cf --- /dev/null +++ b/src/guide/style/dark-theme.sass @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +html[data-theme='dark']:root + --el-color-primary: #409eff + --el-color-primary-light-3: #3375b9 + --el-color-primary-light-5: #2a598a + --el-color-primary-light-7: #213d5b + --el-color-primary-light-8: #1d3043 + --el-color-primary-light-9: #18222c + --el-color-primary-dark-2: #66b1ff + --el-color-success: #67c23a + --el-color-success-light-3: #4e8e2f + --el-color-success-light-5: #3e6b27 + --el-color-success-light-7: #2d481f + --el-color-success-light-8: #25371c + --el-color-success-light-9: #1c2518 + --el-color-success-dark-2: #85ce61 + --el-color-warning: #e6a23c + --el-color-warning-light-3: #a77730 + --el-color-warning-light-5: #7d5b28 + --el-color-warning-light-7: #533f20 + --el-color-warning-light-8: #3e301c + --el-color-warning-light-9: #292218 + --el-color-warning-dark-2: #ebb563 + --el-color-danger: #f56c6c + --el-color-danger-light-3: #b25252 + --el-color-danger-light-5: #854040 + --el-color-danger-light-7: #582e2e + --el-color-danger-light-8: #412626 + --el-color-danger-light-9: #2b1d1d + --el-color-danger-dark-2: #f78989 + --el-color-error: #f56c6c + --el-color-error-light-3: #b25252 + --el-color-error-light-5: #854040 + --el-color-error-light-7: #582e2e + --el-color-error-light-8: #412626 + --el-color-error-light-9: #2b1d1d + --el-color-error-dark-2: #f78989 + --el-color-info: #909399 + --el-color-info-light-3: #6b6d71 + --el-color-info-light-5: #525457 + --el-color-info-light-7: #393a3c + --el-color-info-light-8: #2d2d2f + --el-color-info-light-9: #202121 + --el-color-info-dark-2: #a6a9ad + --el-box-shadow: 0px 12px 32px 4px rgba(0 0 0 .36) 0px 8px 20px rgba(0 0 0 .72) + --el-box-shadow-light: 0px 0px 12px rgba(0 0 0 .72) + --el-box-shadow-lighter: 0px 0px 6px rgba(0 0 0 .72) + --el-box-shadow-dark: 0px 16px 48px 16px rgba(0 0 0 .72) 0px 12px 32px #000000 0px 8px 16px -8px #000000 + --el-bg-color-page: #0a0a0a + --el-bg-color: #141414 + --el-bg-color-overlay: #1d1e1f + --el-text-color-primary: #E5EAF3 + --el-text-color-regular: #CFD3DC + --el-text-color-secondary: #A3A6AD + --el-text-color-placeholder: #8D9095 + --el-text-color-disabled: #6C6E72 + --el-border-color-darker: #636466 + --el-border-color-dark: #58585B + --el-border-color: #4C4D4F + --el-border-color-light: #414243 + --el-border-color-lighter: #363637 + --el-border-color-extra-light: #2B2B2C + --el-fill-color-darker: #424243 + --el-fill-color-dark: #39393A + --el-fill-color: #303030 + --el-fill-color-light: #262727 + --el-fill-color-lighter: #1D1D1D + --el-fill-color-extra-light: #191919 + --el-fill-color-blank: var(--el-fill-color-darker) + --el-mask-color: rgba(0 0 0 .8) + --el-mask-color-extra-light: rgba(0 0 0 .3) + --guide-container-bg-color: var(--el-fill-color-dark) + // menu + --el-menu-bg-color: var(--el-fill-color-light) diff --git a/src/guide/style.sass b/src/guide/style/index.sass similarity index 82% rename from src/guide/style.sass rename to src/guide/style/index.sass index 13dddcee8..b1a9ffe1f 100644 --- a/src/guide/style.sass +++ b/src/guide/style/index.sass @@ -1,10 +1,13 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2023 Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +@import './light-theme' +@import './dark-theme' + body margin: 0px @@ -13,6 +16,8 @@ body height: 100vh width: 100% scroll-behavior: smooth + .guide-container + background: var(--guide-container-bg-color) .el-menu height: 100% .el-main @@ -21,14 +26,14 @@ body .guide-area padding: 0 9vw padding-bottom: 10px + h1,h2,span,div,li + color: var(--el-text-color-primary) h1 - color: #3c4043 font-weight: 500 font-size: 1.8rem letter-spacing: 0 line-height: 1.333 h2 - color: #3c4043 font-weight: 500 font-size: 1.4rem letter-spacing: 0 @@ -36,7 +41,6 @@ body margin: 40px 0 margin-left: 2px .guide-paragragh,.guide-list - color: rgba(0,0,0,.87) font-size: 0.9375rem font-weight: 400 margin-left: 2px @@ -55,7 +59,7 @@ body text-decoration: none cursor: pointer .guide-link,.guide-link:hover,.guide-link:visited - color: #3367d6 + color: var(--el-color-primary) #app-body margin-bottom: 40px diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass new file mode 100644 index 000000000..e75ed01cf --- /dev/null +++ b/src/guide/style/light-theme.sass @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +\:root + --el-menu-bg-color: #1d222d + --el-menu-text-color: #c1c6c8 + --el-menu-hover-bg-color: #262f3e + --guide-container-bg-color: var(--el-fill-color-blank) diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts index b15f86f87..0b0679305 100644 --- a/src/util/dark-mode.ts +++ b/src/util/dark-mode.ts @@ -33,6 +33,6 @@ export function toggle(isDarkMode: boolean) { localStorage.setItem(STORAGE_KEY, isDarkMode ? STORAGE_FLAG : undefined) } -export function isDarkMode() { +function isDarkMode() { return localStorage.getItem(STORAGE_KEY) === STORAGE_FLAG } \ No newline at end of file From 35ff9bdc7b84914b2697698994bf03053f179445 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 5 Jan 2023 21:31:29 +0800 Subject: [PATCH 073/168] Remove running time (#166) --- README-zh.md | 1 - README.md | 1 - global.d.ts | 3 - package.json | 2 +- .../data-manage/clear/filter/index.ts | 6 +- .../clear/filter/operation-button.ts | 7 +- src/app/components/option/components/popup.ts | 6 +- src/app/components/report/file-export.ts | 4 - .../components/report/table/columns/total.ts | 47 ------------ src/app/components/report/table/index.ts | 2 - .../trend/components/chart/wrapper.ts | 8 +- src/background/backup/gist/compressor.ts | 7 +- src/background/backup/gist/gist.d.ts | 1 - src/background/timer/collection-context.ts | 25 +------ src/background/timer/collector.ts | 52 ++++++------- src/background/timer/context.ts | 37 ++------- src/background/timer/idle-listener.ts | 2 +- src/background/timer/save.ts | 24 +++--- src/background/timer/timer.d.ts | 4 + .../0-1-2/host-merge-initializer.ts | 4 +- .../0-7-0/local-file-initializer.ts | 3 +- .../1-4-3/running-time-clear.ts | 38 ++++++++++ src/background/version-manager/index.ts | 5 +- ...sion-processor.ts => version-manager.d.ts} | 6 +- src/content-script/limit.ts | 5 +- src/content-script/locale.ts | 15 ++++ src/content-script/printer.ts | 17 ++--- src/database/timer-database.ts | 72 +++++++++--------- src/i18n/chrome/message.ts | 16 ---- src/i18n/message/app/data-manage.ts | 5 -- src/i18n/message/common/content-script.ts | 16 ++-- src/i18n/message/common/item.ts | 13 ---- src/i18n/message/common/meta.ts | 2 +- src/popup/components/footer/index.ts | 5 +- src/service/timer-service.ts | 12 +-- src/util/constant/url.ts | 6 -- src/util/stat.ts | 11 ++- .../background/backup/gist/compressor.test.ts | 15 ++-- test/database/timer-database.test.ts | 75 ++++++++----------- test/util/stat.test.ts | 5 +- 40 files changed, 225 insertions(+), 360 deletions(-) delete mode 100644 src/app/components/report/table/columns/total.ts create mode 100644 src/background/timer/timer.d.ts create mode 100644 src/background/version-manager/1-4-3/running-time-clear.ts rename src/background/version-manager/{i-version-processor.ts => version-manager.d.ts} (82%) create mode 100644 src/content-script/locale.ts diff --git a/README-zh.md b/README-zh.md index c69d8e463..c25de261d 100644 --- a/README-zh.md +++ b/README-zh.md @@ -10,7 +10,6 @@ 网费很贵是一款用于上网时间统计的浏览器插件,使用 webpack,TypeScript 和 Element-plus 进行开发。你可以在 Firefox,Chrome 和 Edge 中安装并使用它。 -- 统计网站的运行时间 - 统计用户在不同网站上的浏览时间,和访问次数 - 统计用户阅读本地文件的时间 - 网站白名单,过滤不需要统计的网站 diff --git a/README.md b/README.md index e24722cf3..1416e381c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Timer is a browser extension to track the time you spent on all websites. It's b Timer can -- help you count the time each website running in your browser. - help you count the time you browsing and the visit count on each website. - help you count the time you read local files with browsers. - help you analyze your browsing habit by time period and display it as a histogram. diff --git a/global.d.ts b/global.d.ts index 9a1fcdcab..2eb2acbb4 100644 --- a/global.d.ts +++ b/global.d.ts @@ -207,9 +207,6 @@ declare namespace timer { * The dimension to statistics */ type Dimension = - // Running time - // @deprecated v1.3.4 - | 'total' // Focus time | 'focus' // Visit count diff --git a/package.json b/package.json index 4287c868a..61bd786af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.2", + "version": "1.4.3", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { diff --git a/src/app/components/data-manage/clear/filter/index.ts b/src/app/components/data-manage/clear/filter/index.ts index 086257f2c..0876dea53 100644 --- a/src/app/components/data-manage/clear/filter/index.ts +++ b/src/app/components/data-manage/clear/filter/index.ts @@ -20,8 +20,6 @@ const timerDatabase = new TimerDatabase(chrome.storage.local) const dateRangeRef: Ref> = ref([]) const focusStartRef: Ref = ref('0') const focusEndRef: Ref = ref('2') -const totalStartRef: Ref = ref('0') -const totalEndRef: Ref = ref('') const timeStartRef: Ref = ref('0') const timeEndRef: Ref = ref('') @@ -30,7 +28,6 @@ const title = h('h3', t(msg => msg.dataManage.filterItems)) const filterRefs: BaseFilterProps = { dateRangeRef, focusStartRef, focusEndRef, - totalStartRef, totalEndRef, timeStartRef, timeEndRef, } @@ -61,8 +58,7 @@ const _default = defineComponent((_props, ctx: SetupContext) => { title, dateFilter({ dateRangeRef }), numberFilter('filterFocus', focusStartRef, focusEndRef, 2), - numberFilter('filterTotal', totalStartRef, totalEndRef, 3), - numberFilter('filterTime', timeStartRef, timeEndRef, 4), + numberFilter('filterTime', timeStartRef, timeEndRef, 3), footer() ] ) 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 16a697df3..9027a225e 100644 --- a/src/app/components/data-manage/clear/filter/operation-button.ts +++ b/src/app/components/data-manage/clear/filter/operation-button.ts @@ -18,8 +18,6 @@ import { ElementButtonType } from "@src/element-ui/button" const timerDatabase = new TimerDatabase(chrome.storage.local) export type BaseFilterProps = { - totalStartRef: Ref - totalEndRef: Ref focusStartRef: Ref focusEndRef: Ref timeStartRef: Ref @@ -80,10 +78,8 @@ const str2Range = (startAndEnd: Ref[], numAmplifier?: (origin: number) = const seconds2Milliseconds = (a: number) => a * 1000 function checkParam(props: _Props): TimerCondition | undefined { - const { totalStartRef, totalEndRef, focusStartRef, focusEndRef, timeStartRef, timeEndRef, dateRangeRef } = props + const { focusStartRef, focusEndRef, timeStartRef, timeEndRef } = props let hasError = false - const totalRange = str2Range([totalStartRef, totalEndRef], seconds2Milliseconds) - hasError = hasError || assertQueryParam(totalRange) const focusRange = str2Range([focusStartRef, focusEndRef], seconds2Milliseconds) hasError = hasError || assertQueryParam(focusRange) const timeRange = str2Range([timeStartRef, timeEndRef]) @@ -92,7 +88,6 @@ function checkParam(props: _Props): TimerCondition | undefined { return undefined } const condition: TimerCondition = {} - condition.totalRange = totalRange condition.focusRange = focusRange condition.timeRange = timeRange return condition diff --git a/src/app/components/option/components/popup.ts b/src/app/components/option/components/popup.ts index 036e5b7af..e14c971cb 100644 --- a/src/app/components/option/components/popup.ts +++ b/src/app/components/option/components/popup.ts @@ -103,11 +103,7 @@ const _default = defineComponent({ name: "PopupOptionContainer", setup(_props, ctx) { const option: UnwrapRef = reactive(defaultPopup()) - optionService.getAllOption().then(currentVal => { - // Remove total @since v1.3.4 - currentVal.defaultType === 'total' && (currentVal.defaultType = 'focus') - copy(option, currentVal) - }) + optionService.getAllOption().then(currentVal => copy(option, currentVal)) ctx.expose({ async reset() { copy(option, defaultPopup()) diff --git a/src/app/components/report/file-export.ts b/src/app/components/report/file-export.ts index 9ac26cd80..c829201e4 100644 --- a/src/app/components/report/file-export.ts +++ b/src/app/components/report/file-export.ts @@ -17,7 +17,6 @@ type _ExportInfo = { host: string alias?: string date?: string - total?: string focus?: string time?: number } @@ -45,7 +44,6 @@ const generateJsonData = (rows: timer.stat.Row[]) => rows.map(row => { data.date = row.date data.alias = row.alias // Always display by seconds - data.total = periodFormatter(row.total, "second", true) data.focus = periodFormatter(row.focus, "second", true) data.time = row.time return data @@ -73,7 +71,6 @@ function generateCsvData(rows: timer.stat.Row[], filterParam: timer.app.report.F if (!mergeHost) { columnName.push(t(msg => msg.siteManage.column.alias)) } - columnName.push(t(msg => msg.item.total)) columnName.push(t(msg => msg.item.focus)) columnName.push(t(msg => msg.item.time)) const data = [columnName] @@ -86,7 +83,6 @@ function generateCsvData(rows: timer.stat.Row[], filterParam: timer.app.report.F if (!mergeHost) { line.push(row.alias || '') } - line.push(periodFormatter(row.total, "second", true)) line.push(periodFormatter(row.focus, "second", true)) line.push(row.time) data.push(line) diff --git a/src/app/components/report/table/columns/total.ts b/src/app/components/report/table/columns/total.ts deleted file mode 100644 index f00d4b2d5..000000000 --- a/src/app/components/report/table/columns/total.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { PropType } from "vue" - -import { t } from "@app/locale" -import { ElIcon, ElTableColumn, ElTooltip } from "element-plus" -import { defineComponent, h } from "vue" -import { periodFormatter } from "../../formatter" -import { QuestionFilled } from "@element-plus/icons-vue" - -function renderHeader() { - return [ - t(msg => msg.item.total), - h(ElTooltip, { - content: t(msg => msg.item.totalTip), - placement: 'top', - }, () => h(ElIcon, { - style: { paddingLeft: '3px' }, - }, () => h(QuestionFilled))) - ] -} - -const _default = defineComponent({ - name: "TotalColumn", - props: { - timeFormat: { - type: String as PropType - } - }, - setup(props) { - return () => h(ElTableColumn, { - prop: "total", - minWidth: 130, - align: "center", - }, { - header: renderHeader, - default: ({ row }: { row: timer.stat.Row }) => periodFormatter(row.total, props.timeFormat) - }) - } -}) - -export default _default \ No newline at end of file diff --git a/src/app/components/report/table/index.ts b/src/app/components/report/table/index.ts index 2ad98a482..740c63797 100644 --- a/src/app/components/report/table/index.ts +++ b/src/app/components/report/table/index.ts @@ -14,7 +14,6 @@ import DateColumn from "./columns/date" import HostColumn from "./columns/host" import AliasColumn from "./columns/alias" import FocusColumn from "./columns/focus" -import TotalColumn from "./columns/total" import TimeColumn from "./columns/time" import OperationColumn from "./columns/operation" @@ -68,7 +67,6 @@ const _default = defineComponent({ })) result.push(h(FocusColumn, { timeFormat: props.timeFormat })) result.push(h(TimeColumn)) - result.push(h(TotalColumn, { timeFormat: props.timeFormat })) result.push(h(OperationColumn, { mergeDate: props.mergeDate, mergeHost: props.mergeHost, diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts index 78de978bd..1a64eedac 100644 --- a/src/app/components/trend/components/chart/wrapper.ts +++ b/src/app/components/trend/components/chart/wrapper.ts @@ -135,7 +135,7 @@ function optionOf( ], legend: [{ left: 'left', - data: [t(msg => msg.item.total), t(msg => msg.item.focus), t(msg => msg.item.time)], + data: [t(msg => msg.item.focus), t(msg => msg.item.time)], textStyle: { color: textColor }, }], series: [{ @@ -243,12 +243,6 @@ class ChartWrapper { const dataItems = processDataItems(allDates, timeFormat, rows) const option: EcOption = optionOf(xAxisData, subtitle, timeFormat, dataItems) - if (renderOption.isFirst) { - // Close the running time by default - const closedLegends: Record = {} - closedLegends[t(msg => msg.item.total)] = false - option.legend[0].selected = closedLegends - } this.instance?.setOption(option) } diff --git a/src/background/backup/gist/compressor.ts b/src/background/backup/gist/compressor.ts index d8c291a01..661a8985c 100644 --- a/src/background/backup/gist/compressor.ts +++ b/src/background/backup/gist/compressor.ts @@ -19,7 +19,7 @@ function compress(rows: timer.stat.RowBase[]): GistData { row => row.date.substring(6), groupedRows => { const gistRow: GistRow = {} - groupedRows.forEach(({ host, focus, total, time }) => gistRow[host] = [time, focus, total]) + groupedRows.forEach(({ host, focus, time }) => gistRow[host] = [time, focus]) return gistRow } ) @@ -48,11 +48,12 @@ export function gistData2Rows(yearMonth: string, gistData: GistData): timer.stat Object.entries(gistData).forEach(([dateOfMonth, gistRow]) => { const date = yearMonth + dateOfMonth Object.entries(gistRow).forEach(([host, val]) => { - const [time, focus, total] = val + const [time, focus] = val const row: timer.stat.RowBase = { date, host, - time, focus, total + time, + focus } result.push(row) }) diff --git a/src/background/backup/gist/gist.d.ts b/src/background/backup/gist/gist.d.ts index cb5840ff8..d3ec491f8 100644 --- a/src/background/backup/gist/gist.d.ts +++ b/src/background/backup/gist/gist.d.ts @@ -15,6 +15,5 @@ declare type GistRow = { [host: string]: [ number, // Visit count number, // Browsing time - number // Running time ] } \ No newline at end of file diff --git a/src/background/timer/collection-context.ts b/src/background/timer/collection-context.ts index bd7d9a68b..d802b379b 100644 --- a/src/background/timer/collection-context.ts +++ b/src/background/timer/collection-context.ts @@ -5,17 +5,12 @@ * https://opensource.org/licenses/MIT */ -import TimerContext, { TimeInfo } from "./context" +import TimerContext from "./context" export default class CollectionContext { realInterval: number timerContext: TimerContext - hostSet: Set - /** - * The focus host while last collection - */ - focusHost: string - focusUrl: string + init() { const now = Date.now() @@ -25,22 +20,10 @@ export default class CollectionContext { constructor() { this.timerContext = new TimerContext() - this.hostSet = new Set() this.init() } - collectHost(host: string) { this.hostSet.add(host) } - - resetFocus(focusHost: string, focusUrl: string) { - this.focusHost = focusHost - this.focusUrl = focusUrl - } - - accumulateAll() { - const interval = this.realInterval - this.hostSet.forEach((host: string) => { - const info = TimeInfo.of(interval, this.focusHost === host ? interval : 0) - this.timerContext.accumulate(host, info) - }) + accumulate(focusHost: string, focusUrl: string) { + this.timerContext.accumulate(focusHost, focusUrl, this.realInterval) } } \ No newline at end of file diff --git a/src/background/timer/collector.ts b/src/background/timer/collector.ts index da9587c7f..3878b6dfa 100644 --- a/src/background/timer/collector.ts +++ b/src/background/timer/collector.ts @@ -13,24 +13,18 @@ let countLocalFiles: boolean optionService.getAllOption().then(option => countLocalFiles = !!option.countLocalFiles) optionService.addOptionChangeListener((newVal => countLocalFiles = !!newVal.countLocalFiles)) -/** - * The promise for window query - */ -function WindowPromise(window: chrome.windows.Window, context: CollectionContext) { - return new Promise(resolve => handleWindow(resolve, window, context)) +function queryAllWindows(): Promise { + return new Promise(resolve => chrome.windows.getAll(resolve)) } -function handleWindow(resolve: (val?: unknown) => void, window: chrome.windows.Window, context: CollectionContext) { - const windowId = window.id - const windowFocused = !!window.focused - chrome.tabs.query({ windowId }, tabs => { - if (chrome.runtime.lastError) { /** prevent it from throwing error */ } - // tabs maybe undefined - if (!tabs) return - tabs.forEach(tab => handleTab(tab, windowFocused, context)) - resolve() - }) + +function queryAllTabs(windowId: number): Promise { + return new Promise(resolve => chrome.tabs.query({ windowId }, resolve)) } -function handleTab(tab: chrome.tabs.Tab, isFocusWindow: boolean, context: CollectionContext) { + +function handleTab(tab: chrome.tabs.Tab, window: chrome.windows.Window, context: CollectionContext) { + if (!tab.active || !window.focused) { + return + } const url = tab.url if (!url) return if (isBrowserUrl(url)) return @@ -40,14 +34,24 @@ function handleTab(tab: chrome.tabs.Tab, isFocusWindow: boolean, context: Collec host = extractFileHost(url) } if (host) { - context.collectHost(host) - const isFocus = isFocusWindow && tab.active - isFocus && context.resetFocus(host, url) + context.accumulate(host, url) } else { console.log('Detect blank host:', url) } } +async function doCollect(context: CollectionContext) { + const windows = await queryAllWindows() + for (const window of windows) { + const tabs = await queryAllTabs(window.id) + // tabs maybe undefined + if (!tabs) { + continue + } + tabs.forEach(tab => handleTab(tab, window, context)) + } +} + export default class TimeCollector { context: CollectionContext @@ -58,14 +62,6 @@ export default class TimeCollector { collect() { this.context.init() if (this.context.timerContext.isPaused()) return - chrome.windows.getAll(windows => processWindows(windows, this.context)) + doCollect(this.context) } -} - -async function processWindows(windows: chrome.windows.Window[], context: CollectionContext) { - context.focusHost = '' - const windowPromises = windows.map(w => WindowPromise(w, context)) - await Promise.all(windowPromises) - // Accumulate the time of all the hosts - context.accumulateAll() } \ No newline at end of file diff --git a/src/background/timer/context.ts b/src/background/timer/context.ts index 29f997ab0..75466ef4b 100644 --- a/src/background/timer/context.ts +++ b/src/background/timer/context.ts @@ -7,27 +7,6 @@ import optionService from "@service/option-service" -export class TimeInfo { - focus: number - run: number - - static zero() { - return this.of(0, 0) - } - - static of(run: number, focus?: number): TimeInfo { - const info = new TimeInfo() - info.run = run || 0 - info.focus = focus || 0 - return info - } - - selfIncrease(another: TimeInfo): void { - this.focus += another.focus - this.run += another.run - } -} - let countWhenIdle: boolean = false const setCountWhenIdle = (op: timer.option.AllOption) => countWhenIdle = op.countWhenIdle @@ -55,10 +34,11 @@ export default class TimerContext { this.resetTimeMap() } - accumulate(host: string, timeInfo: TimeInfo) { - let data = this.timeMap[host] - !data && (this.timeMap[host] = data = TimeInfo.zero()) - data.selfIncrease(timeInfo) + accumulate(host: string, url: string, focusTime: number) { + let data: TimeInfo = this.timeMap[host] + !data && (this.timeMap[host] = data = {}) + let existFocusTime = data[url] || 0 + data[url] = existFocusTime + focusTime } /** @@ -66,13 +46,6 @@ export default class TimerContext { */ resetTimeMap(): void { this.timeMap = {} } - /** - * @returns The focus info - */ - findFocus(): [host: string, timeInfo: TimeInfo] | undefined { - return Object.entries(this.timeMap).find(([_host, { focus }]) => focus) - } - setIdle(idleNow: chrome.idle.IdleState) { this.idleState = idleNow } isPaused(): boolean { diff --git a/src/background/timer/idle-listener.ts b/src/background/timer/idle-listener.ts index 0941cfc43..445a26950 100644 --- a/src/background/timer/idle-listener.ts +++ b/src/background/timer/idle-listener.ts @@ -26,7 +26,7 @@ export default class IdleListener { listen() { if (!IS_SAFARI) { - // Idle does not work in macOs + // Idle does not work in Safari chrome.idle.onStateChanged.addListener(newState => listen(this.context, newState)) } } diff --git a/src/background/timer/save.ts b/src/background/timer/save.ts index bda825601..7467a78fa 100644 --- a/src/background/timer/save.ts +++ b/src/background/timer/save.ts @@ -8,6 +8,7 @@ import limitService from "@service/limit-service" import periodService from "@service/period-service" import timerService from "@service/timer-service" +import { sum } from "@util/array" import CollectionContext from "./collection-context" function sendLimitedMessage(item: timer.limit.Item[]) { @@ -30,16 +31,19 @@ function sendLimitedMessage(item: timer.limit.Item[]) { export default async function save(collectionContext: CollectionContext) { const context = collectionContext.timerContext if (context.isPaused()) return - timerService.addFocusAndTotal(context.timeMap) - const focusEntry = context.findFocus() - - if (focusEntry) { - // Add period time - periodService.add(context.lastCollectTime, focusEntry[1].focus) - // Add limit time - const limitedRules = await limitService.addFocusTime(collectionContext.focusHost, collectionContext.focusUrl, focusEntry[1].focus) - // If time limited after this operation, send messages - limitedRules && limitedRules.length && sendLimitedMessage(limitedRules) + const timeMap = context.timeMap + timerService.addFocusAndTotal(timeMap) + const totalFocusTime = sum(Object.values(timeMap).map(timeInfo => sum(Object.values(timeInfo)))) + // Add period time + await periodService.add(context.lastCollectTime, totalFocusTime) + for (const [host, timeInfo] of Object.entries(timeMap)) { + for (const [url, focusTime] of Object.entries(timeInfo)) { + // Add limit time + const limitedRules = await limitService.addFocusTime(host, url, focusTime) + // If time limited after this operation, send messages + limitedRules && limitedRules.length && sendLimitedMessage(limitedRules) + } } + context.resetTimeMap() } diff --git a/src/background/timer/timer.d.ts b/src/background/timer/timer.d.ts new file mode 100644 index 000000000..80faa79c2 --- /dev/null +++ b/src/background/timer/timer.d.ts @@ -0,0 +1,4 @@ + +declare type TimeInfo = { + [url: string]: number // Focus time +} \ No newline at end of file diff --git a/src/background/version-manager/0-1-2/host-merge-initializer.ts b/src/background/version-manager/0-1-2/host-merge-initializer.ts index fd3c27529..8c87a2aaa 100644 --- a/src/background/version-manager/0-1-2/host-merge-initializer.ts +++ b/src/background/version-manager/0-1-2/host-merge-initializer.ts @@ -6,7 +6,6 @@ */ import MergeRuleDatabase from "@db/merge-rule-database" -import IVersionProcessor from "../i-version-processor" const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local) @@ -15,7 +14,8 @@ const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local) * * Initialize the merge rules */ -export default class HostMergeInitializer implements IVersionProcessor { +export default class HostMergeInitializer implements VersionProcessor { + since(): string { return '0.1.2' } 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 021c4d7e7..6bd42f0b2 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 @@ -8,7 +8,6 @@ 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 IVersionProcessor from "../i-version-processor" import { t2Chrome } from "@i18n/chrome/t" const storage: chrome.storage.StorageArea = chrome.storage.local @@ -21,7 +20,7 @@ const hostAliasDatabase = new HostAliasDatabase(storage) * * @since 0.7.0 */ -export default class LocalFileInitializer implements IVersionProcessor { +export default class LocalFileInitializer implements VersionProcessor { since(): string { return '0.7.0' } 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 new file mode 100644 index 000000000..cb7b9a1de --- /dev/null +++ b/src/background/version-manager/1-4-3/running-time-clear.ts @@ -0,0 +1,38 @@ +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/index.ts b/src/background/version-manager/index.ts index 9aca5cd36..9378ccda1 100644 --- a/src/background/version-manager/index.ts +++ b/src/background/version-manager/index.ts @@ -5,9 +5,9 @@ * https://opensource.org/licenses/MIT */ -import IVersionProcessor from "./i-version-processor" 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" /** * Version manager @@ -15,12 +15,13 @@ import LocalFileInitializer from "./0-7-0/local-file-initializer" * @since 0.1.2 */ class VersionManager { - processorChain: IVersionProcessor[] = [] + processorChain: VersionProcessor[] = [] constructor() { this.processorChain.push( new HostMergeInitializer(), new LocalFileInitializer(), + new RunningTimeClear(), ) this.processorChain = this.processorChain.sort((a, b) => a.since() >= b.since() ? 1 : 0) } diff --git a/src/background/version-manager/i-version-processor.ts b/src/background/version-manager/version-manager.d.ts similarity index 82% rename from src/background/version-manager/i-version-processor.ts rename to src/background/version-manager/version-manager.d.ts index e9e8b0bb4..18ca6519b 100644 --- a/src/background/version-manager/i-version-processor.ts +++ b/src/background/version-manager/version-manager.d.ts @@ -1,6 +1,6 @@ /** - * Copyright (c) 2021 Hengyang Zhang - * + * Copyright (c) 2022 Hengyang Zhang + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -10,7 +10,7 @@ * * @since 0.1.2 */ -export default interface IVersionProcessor { +declare type VersionProcessor = { /** * The version number of this processor */ diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index fcb282449..44d81cd8b 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -8,6 +8,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 = { @@ -65,7 +66,7 @@ class _Modal { const link = document.createElement('a') Object.assign(link.style, linkStyle) link.setAttribute('href', 'javascript:void(0)') - const text = t2Chrome(msg => msg.message.more5Minutes) + const text = t(msg => msg.more5Minutes) link.innerText = text link.onclick = async () => { const delayRules = await moreMinutes(_thisUrl) @@ -150,7 +151,7 @@ function link2Setup(url: string): HTMLParagraphElement { const link = document.createElement('a') Object.assign(link.style, linkStyle) link.setAttribute('href', 'javascript:void(0)') - const text = t2Chrome(msg => msg.message.timeLimitMsg) + const text = t(msg => msg.timeLimitMsg) .replace('{appName}', t2Chrome(msg => msg.meta.name)) link.innerText = text link.onclick = () => chrome.runtime.sendMessage(openLimitPageMessage(url)) diff --git a/src/content-script/locale.ts b/src/content-script/locale.ts new file mode 100644 index 000000000..2aa3a5302 --- /dev/null +++ b/src/content-script/locale.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import type { I18nKey } from "@i18n" +import type { ContentScriptMessage } from "@i18n/message/common/content-script" + +import { t as t_ } from "@i18n" +import messages from "@i18n/message/common/content-script" + +export function t(key: I18nKey): string { + return t_(messages, { key }) +} \ No newline at end of file diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 8e1f9c1a1..670c6933b 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { t2Chrome } from "@i18n/chrome/t" +import { t } from "./locale" import { formatPeriod } from "@util/time" function getTodayInfo(host: string): Promise { @@ -24,20 +24,17 @@ function getTodayInfo(host: string): Promise { */ export default async function printInfo(host: string) { const waste: timer.stat.Result = await getTodayInfo(host) - const hourMsg = t2Chrome(root => root.message.timeWithHour) - const minuteMsg = t2Chrome(root => root.message.timeWithMinute) - const secondMsg = t2Chrome(root => root.message.timeWithSecond) + const hourMsg = t(msg => msg.timeWithHour) + const minuteMsg = t(msg => msg.timeWithMinute) + const secondMsg = t(msg => msg.timeWithSecond) const msg = { hourMsg, minuteMsg, secondMsg } - const info0 = t2Chrome(root => root.message.openTimesConsoleLog) + const info0 = t(msg => msg.consoleLog) .replace('{time}', waste.time ? '' + waste.time : '-') - .replace('{host}', host) - const info1 = t2Chrome(root => root.message.usedTimeInConsoleLog) .replace('{focus}', formatPeriod(waste.focus, msg)) - .replace('{total}', formatPeriod(waste.total, msg)) - const info2 = t2Chrome(root => root.message.closeAlert) + .replace('{host}', host) + const info1 = t(msg => msg.closeAlert) console.log(info0) console.log(info1) - console.log(info2) } \ No newline at end of file diff --git a/src/database/timer-database.ts b/src/database/timer-database.ts index 7e1c4e265..bead4cfdb 100644 --- a/src/database/timer-database.ts +++ b/src/database/timer-database.ts @@ -21,13 +21,6 @@ export type TimerCondition = { * Host name for query */ host?: string - /** - * Total range, milliseconds - * - * @since 0.0.9 - * @deprecated 1.3.4 - */ - totalRange?: number[] /** * Focus range, milliseconds * @@ -60,8 +53,6 @@ type _TimerCondition = TimerCondition & { timeEnd?: number focusStart?: number focusEnd?: number - totalStart?: number - totalEnd?: number } function processDateCondition(cond: _TimerCondition, paramDate: Date | Date[]) { @@ -87,11 +78,7 @@ function processParamTimeCondition(cond: _TimerCondition, paramTime: number[]) { paramTime.length >= 2 && (cond.timeEnd = paramTime[1]) paramTime.length >= 1 && (cond.timeStart = paramTime[0]) } -function processParamTotalCondition(cond: _TimerCondition, paramTotal: number[]) { - if (!paramTotal) return - paramTotal.length >= 2 && (cond.totalEnd = paramTotal[1]) - paramTotal.length >= 1 && (cond.totalStart = paramTotal[0]) -} + function processParamFocusCondition(cond: _TimerCondition, paramFocus: number[]) { if (!paramFocus) return paramFocus.length >= 2 && (cond.focusEnd = paramFocus[1]) @@ -102,14 +89,24 @@ function processCondition(condition: TimerCondition): _TimerCondition { const result: _TimerCondition = { ...condition } processDateCondition(result, condition.date) processParamTimeCondition(result, condition.timeRange) - processParamTotalCondition(result, condition.totalRange) processParamFocusCondition(result, condition.focusRange) return result } function mergeMigration(exist: timer.stat.Result | undefined, another: any) { exist = exist || createZeroResult() - return mergeResult(exist, { total: another.total || 0, focus: another.focus || 0, time: another.time || 0 }) + return mergeResult(exist, { focus: another.focus || 0, time: another.time || 0 }) +} + +/** + * Generate the key in local storage by host and date + * + * @param host host + * @param date date + */ +function generateKey(host: string, date: Date | string) { + const str = typeof date === 'object' ? formatTime(date as Date, DATE_FORMAT) : date + return str + host } function migrate(exists: { [key: string]: timer.stat.Result }, data: any): { [key: string]: timer.stat.Result } { @@ -141,23 +138,12 @@ class TimerDatabase extends BaseDatabase { return Promise.resolve(items) } - /** - * Generate the key in local storage by host and date - * - * @param host host - * @param date date - */ - private generateKey(host: string, date: Date | string) { - const str = typeof date === 'object' ? formatTime(date as Date, DATE_FORMAT) : date - return str + host - } - /** * @param host host * @since 0.1.3 */ async accumulate(host: string, date: Date | string, item: timer.stat.Result): Promise { - const key = this.generateKey(host, date) + const key = generateKey(host, date) const items = await this.storage.get(key) const exist: timer.stat.Result = mergeResult(items[key] as timer.stat.Result || createZeroResult(), item) const toUpdate = {} @@ -178,7 +164,7 @@ class TimerDatabase extends BaseDatabase { if (!hosts.length) return const dateStr = formatTime(date, DATE_FORMAT) const keys: { [host: string]: string } = {} - hosts.forEach(host => keys[host] = this.generateKey(host, dateStr)) + hosts.forEach(host => keys[host] = generateKey(host, dateStr)) const items = await this.storage.get(Object.values(keys)) @@ -210,8 +196,8 @@ class TimerDatabase extends BaseDatabase { const host = key.substring(8) const val: timer.stat.Result = items[key] if (this.filterBefore(date, host, val, _cond)) { - const { total, focus, time } = val - result.push({ date, host, total, focus, time, mergedHosts: [] }) + const { focus, time } = val + result.push({ date, host, focus, time, mergedHosts: [] }) } } @@ -256,13 +242,12 @@ class TimerDatabase extends BaseDatabase { * @return true if valid, or false */ private filterBefore(date: string, host: string, val: timer.stat.Result, condition: _TimerCondition): boolean { - const { focus, total, time } = val - const { timeStart, timeEnd, totalStart, totalEnd, focusStart, focusEnd } = condition + const { focus, time } = val + const { timeStart, timeEnd, focusStart, focusEnd } = condition return this.filterHost(host, condition) && this.filterDate(date, condition) && this.filterNumberRange(time, [timeStart, timeEnd]) - && this.filterNumberRange(total, [totalStart, totalEnd]) && this.filterNumberRange(focus, [focusStart, focusEnd]) } @@ -272,8 +257,8 @@ class TimerDatabase extends BaseDatabase { * @since 0.0.5 */ async get(host: string, date: Date): Promise { - const key = this.generateKey(host, date) - const items = await this.storage.get(null) + const key = generateKey(host, date) + const items = await this.storage.get(key) return Promise.resolve(items[key] || createZeroResult()) } @@ -285,7 +270,7 @@ class TimerDatabase extends BaseDatabase { * @since 0.0.5 */ async deleteByUrlAndDate(host: string, date: Date | string): Promise { - const key = this.generateKey(host, date) + const key = generateKey(host, date) return this.storage.remove(key) } @@ -296,10 +281,21 @@ class TimerDatabase extends BaseDatabase { * @since 0.0.9 */ async delete(rows: timer.stat.Row[]): Promise { - const keys: string[] = rows.filter(({ date, host }) => !!host && !!date).map(({ host, date }) => this.generateKey(host, date)) + const keys: string[] = rows.filter(({ date, host }) => !!host && !!date).map(({ host, date }) => generateKey(host, date)) return this.storage.remove(keys) } + /** + * Force update data + * + * @since 1.4.3 + */ + forceUpdate(row: timer.stat.Row): Promise { + const key = generateKey(row.host, row.date) + const result: timer.stat.Result = { time: row.time, focus: row.focus } + return this.storage.put(key, result) + } + /** * @param host host * @param start start date, inclusive diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index c04c2dca8..9ef326c60 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -6,7 +6,6 @@ */ import metaMessages, { MetaMessage } from "../message/common/meta" -import contentScriptMessages, { ContentScriptMessage } from "../message/common/content-script" import contextMenusMessages, { ContextMenusMessage } from "../message/common/context-menus" import initialMessages, { InitialMessage } from "../message/common/initial" import baseMessages, { BaseMessage } from "@i18n/message/common/base" @@ -14,7 +13,6 @@ import baseMessages, { BaseMessage } from "@i18n/message/common/base" export type ChromeMessage = { meta: MetaMessage base: BaseMessage - message: ContentScriptMessage contextMenus: ContextMenusMessage initial: InitialMessage } @@ -23,28 +21,24 @@ const messages: Messages = { zh_CN: { meta: metaMessages.zh_CN, base: baseMessages.zh_CN, - message: contentScriptMessages.zh_CN, contextMenus: contextMenusMessages.zh_CN, initial: initialMessages.zh_CN, }, zh_TW: { meta: metaMessages.zh_TW, base: baseMessages.zh_TW, - message: contentScriptMessages.zh_TW, contextMenus: contextMenusMessages.zh_TW, initial: initialMessages.zh_TW, }, en: { meta: metaMessages.en, base: baseMessages.en, - message: contentScriptMessages.en, contextMenus: contextMenusMessages.en, initial: initialMessages.en, }, ja: { meta: metaMessages.ja, base: baseMessages.ja, - message: contentScriptMessages.ja, contextMenus: contextMenusMessages.ja, initial: initialMessages.ja, } @@ -63,16 +57,6 @@ const placeholder: ChromeMessage = { allFunction: '', guidePage: '', }, - message: { - openTimesConsoleLog: '', - usedTimeInConsoleLog: '', - closeAlert: '', - timeWithHour: '', - timeWithMinute: '', - timeWithSecond: '', - timeLimitMsg: '', - more5Minutes: '' - }, contextMenus: { add2Whitelist: '', removeFromWhitelist: '', diff --git a/src/i18n/message/app/data-manage.ts b/src/i18n/message/app/data-manage.ts index c7f36eb81..726cc5e14 100644 --- a/src/i18n/message/app/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -12,7 +12,6 @@ export type DataManageMessage = { operationAlert: string filterItems: string filterFocus: string - filterTotal: string filterTime: string filterDate: string unlimited: string @@ -39,7 +38,6 @@ const _default: Messages = { operationAlert: '您可以归档或者删除那些无关紧要的数据,来减小内存空间', filterItems: '数据筛选', filterFocus: '当日阅览时间在 {start} 秒至 {end} 秒之间。', - filterTotal: '当日运行时间在 {start} 秒至 {end} 秒之间。', filterTime: '当日打开次数在 {start} 次至 {end} 次之间。', filterDate: '{picker} 产生的数据。', unlimited: '无限', @@ -64,7 +62,6 @@ const _default: Messages = { operationAlert: '您可以歸檔或者刪除那些無關緊要的數據,來減小內存空間', filterItems: '數據篩選', filterFocus: '當日閱覽時間在 {start} 秒至 {end} 秒之間。', - filterTotal: '當日運行時間在 {start} 秒至 {end} 秒之間。', filterTime: '當日打開次數在 {start} 次至 {end} 次之間。', filterDate: '{picker} 産生的數據。', unlimited: '無限', @@ -89,7 +86,6 @@ const _default: Messages = { operationAlert: 'You can delete those unimportant data to reduce memory usage', filterItems: 'Filter data', filterFocus: 'The browsing time of the day is between {start} seconds and {end} seconds', - filterTotal: 'The running time of the day is between {start} seconds and {end} seconds', filterTime: 'The number of visits for the day is between {start} and {end}', filterDate: 'Recorded between {picker}', unlimited: '∞', @@ -114,7 +110,6 @@ const _default: Messages = { operationAlert: '不要なデータを削除してメモリ容量を減らすことができます', filterItems: 'データフィルタリング', filterFocus: '当日の閲覧時間は、{start} 秒から {end} 秒の間です。', - filterTotal: '当日の実行時間は、{start} 秒から {end} 秒の間です。', filterTime: '当日のオープン数は {start} から {end} の間です。', filterDate: '{picker} までに生成されたデータ', unlimited: '無制限', diff --git a/src/i18n/message/common/content-script.ts b/src/i18n/message/common/content-script.ts index 91db29146..457567bc8 100644 --- a/src/i18n/message/common/content-script.ts +++ b/src/i18n/message/common/content-script.ts @@ -6,8 +6,7 @@ */ export type ContentScriptMessage = { - openTimesConsoleLog: string - usedTimeInConsoleLog: string + consoleLog: string closeAlert: string timeWithHour: string timeWithMinute: string @@ -15,10 +14,10 @@ export type ContentScriptMessage = { timeLimitMsg: string more5Minutes: string } + const _default: Messages = { zh_CN: { - openTimesConsoleLog: '今天您打开了 {time} 次 {host}。', - usedTimeInConsoleLog: '它今天在您的电脑上运行了 {total},其中您花费了 {focus}来浏览它。', + consoleLog: '今天您打开了 {time} 次 {host},花费了 {focus} 来浏览它。', closeAlert: '你可以在【网费很贵】的选项中关闭以上提示!', timeWithHour: '{hour} 小时 {minute} 分 {second} 秒', timeWithMinute: '{minute} 分 {second} 秒', @@ -27,8 +26,7 @@ const _default: Messages = { more5Minutes: '再看 5 分钟!!我保证!', }, zh_TW: { - openTimesConsoleLog: '今天您打開了 {time} 次 {host}。', - usedTimeInConsoleLog: '它今天在您的電腦上運行了 {total},其中您花費了 {focus}來瀏覽它。', + consoleLog: '今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。', closeAlert: '你可以在【網費很貴】的選項中關閉以上提示!', timeWithHour: '{hour} 小時 {minute} 分 {second} 秒', timeWithMinute: '{minute} 分 {second} 秒', @@ -37,8 +35,7 @@ const _default: Messages = { more5Minutes: '再看 5 分鐘!!我保証!', }, en: { - openTimesConsoleLog: 'You have open {host} for {time} time(s) today.', - usedTimeInConsoleLog: 'And it runs on your PC for {total} today, and is browsed for {focus}.', + consoleLog: 'You have open {host} for {time} time(s) and browsed it for {focus} today.', closeAlert: 'You can turn off the above tips in the option of Timer!', timeWithHour: '{hour} hour(s) {minute} minute(s) {second} second(s)', timeWithMinute: '{minute} minute(s) {second} second(s)', @@ -47,8 +44,7 @@ const _default: Messages = { more5Minutes: 'More 5 minutes, please!!', }, ja: { - openTimesConsoleLog: '今日、あなたは {host} を {time} 回開きました。', - usedTimeInConsoleLog: 'それは今日あなたのコンピュータで {total} 実行されました、そのうちあなたはそれを閲覧するために {focus} を費やしました。', + consoleLog: '{host} を {time} 回開いて、今日 {focus} をブラウズしました。', closeAlert: 'Timer のオプションで上記のヒントをオフにすることができます!', timeWithHour: '{hour} 時間 {minute} 分 {second} 秒', timeWithMinute: '{minute} 分 {second} 秒', diff --git a/src/i18n/message/common/item.ts b/src/i18n/message/common/item.ts index 15fc4f98d..0faab152f 100644 --- a/src/i18n/message/common/item.ts +++ b/src/i18n/message/common/item.ts @@ -8,11 +8,6 @@ export type ItemMessage = { date: string host: string - // @deprecated v1.3.4 - total: string - // @since v1.3.4 - // @deprecated v1.3.4 - totalTip: string focus: string time: string operation: { @@ -33,8 +28,6 @@ const _default: Messages = { zh_CN: { date: '日期', host: '域名', - total: '运行时长', - totalTip: '许多用户反应该数据实际作用不大。作者深思熟虑,打算于不久的将来下线它。如果您有疑问,欢迎提交反馈。', focus: '浏览时长', time: '打开次数', operation: { @@ -53,8 +46,6 @@ const _default: Messages = { zh_TW: { date: '日期', host: '域名', - total: '運行時長', - totalTip: '許多用戶反應該數據實際作用不大。作者深思熟慮,打算於不久的將來下線它。如果您有疑問,歡迎提交反饋。', focus: '瀏覽時長', time: '訪問次數', operation: { @@ -73,8 +64,6 @@ const _default: Messages = { en: { date: 'Date', host: 'Site URL', - total: 'Running Time', - totalTip: 'The author plans to take this column offline in the near future. If you have questions, feel free to submit feedback.', focus: 'Browsing Time', time: 'Site Visits', operation: { @@ -93,8 +82,6 @@ const _default: Messages = { ja: { date: '日期', host: 'URL', - total: '実行時間', - totalTip: '著者は、近い将来にオフラインにする予定です。 ご不明な点がございましたら、お気軽にフィードバックを送信してください。', focus: '閲覧時間', time: '拜訪回数', operation: { diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index e5891fdbd..43b9b2b0e 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -23,7 +23,7 @@ const _default: Messages = { }, en: { name: 'Timer', - marketName: 'Timer - Running & Browsing Time & Visit count', + marketName: 'Timer - Browsing Time & Visit count', description: 'To be the BEST web timer.', }, } diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 1cbe6c8a0..560fe2d75 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -82,8 +82,7 @@ class FooterWrapper { const option = await optionService.getAllOption() this.timeSelectWrapper.init(option.defaultDuration) - // Remove total @since v1.3.4 - const defaultType = option.defaultType === 'total' ? 'focus' : option.defaultType + const defaultType = option.defaultType this.typeSelectWrapper.init(defaultType) this.mergeHostWrapper.init(option.defaultMergeDomain) this.query() @@ -98,7 +97,6 @@ class FooterWrapper { const other: timer.popup.Row = { host: t(msg => msg.chart.otherLabel, { count: 0 }), focus: 0, - total: 0, date: '0000-00-00', time: 0, mergedHosts: [], @@ -111,7 +109,6 @@ class FooterWrapper { popupRows.push(row) } else { other.focus += row.focus - other.total += row.total otherCount++ } } diff --git a/src/service/timer-service.ts b/src/service/timer-service.ts index a6404c3ca..8fe4eab2d 100644 --- a/src/service/timer-service.ts +++ b/src/service/timer-service.ts @@ -77,22 +77,26 @@ export type HostSet = { merged: Set } +function calcFocusInfo(timeInfo: TimeInfo): number { + return Object.values(timeInfo).reduce((a, b) => a + b, 0) +} + /** * Service of timer * @since 0.0.5 */ class TimerService { - async addFocusAndTotal(data: { [host: string]: { run: number, focus: number } }): Promise { + async addFocusAndTotal(data: { [host: string]: TimeInfo }): Promise { const toUpdate = {} Object.entries(data) .filter(([host]) => whitelistHolder.notContains(host)) - .forEach(([host, item]) => toUpdate[host] = resultOf(item.run, item.focus, 0)) + .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, 0, 1)) + timerDatabase.accumulate(host, new Date(), resultOf(0, 1)) } /** @@ -246,7 +250,6 @@ class TimerService { if (exist) { exist.focus += row.focus || 0 exist.time += row.time || 0 - exist.total += row.total || 0 } else { originMap[key] = { ...row, mergedHosts: [] } } @@ -329,7 +332,6 @@ class TimerService { } exist.time += origin.time exist.focus += origin.focus - exist.total += origin.total origin.mergedHosts && origin.mergedHosts.forEach(originHost => !exist.mergedHosts.find(existOrigin => existOrigin.host === originHost.host) && exist.mergedHosts.push(originHost) diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 2f2c8036d..966db8498 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -121,12 +121,6 @@ export function iconUrlOfBrowser(protocol: string, host: string): string { */ export const PSL_HOMEPAGE = 'https://publicsuffix.org/' -/** - * @since 0.9.3 - * @deprecated 1.4.0 Use crowdin to manage translations - */ -export const TRANSLATION_ISSUE_PAGE = 'https://docs.google.com/forms/d/e/1FAIpQLSdZSmEZp6Xfmb5v-3H4hsubgeCReDayDOuWDWWU5C1W80exGA/viewform?usp=sf_link' - /** * The id of project on crowdin.com * diff --git a/src/util/stat.ts b/src/util/stat.ts index 7d8a0e01a..2bf0357eb 100644 --- a/src/util/stat.ts +++ b/src/util/stat.ts @@ -6,26 +6,25 @@ */ export function isNotZeroResult(target: timer.stat.Result): boolean { - return !!target.total || !!target.focus || !!target.time + return !!target.focus || !!target.time } export function createZeroResult(): timer.stat.Result { - return { focus: 0, time: 0, total: 0 } + return { focus: 0, time: 0 } } export function mergeResult(a: timer.stat.Result, b: timer.stat.Result): timer.stat.Result { - return { total: a.total + b.total, focus: a.focus + b.focus, time: a.time + b.time } + return { focus: a.focus + b.focus, time: a.time + b.time } } -export function resultOf(total: number, focus: number, time: number): timer.stat.Result { - return { total, focus, time } +export function resultOf(focus: number, time: number): timer.stat.Result { + return { focus, time } } export function rowOf(key: timer.stat.RowKey, item?: timer.stat.Result): timer.stat.Row { return { host: key.host, date: key.date, - total: item && item.total || 0, focus: item && item.focus || 0, time: item && item.time || 0, mergedHosts: [] diff --git a/test/background/backup/gist/compressor.test.ts b/test/background/backup/gist/compressor.test.ts index c8fac1012..c06d4670b 100644 --- a/test/background/backup/gist/compressor.test.ts +++ b/test/background/backup/gist/compressor.test.ts @@ -6,7 +6,6 @@ test('devide 1', () => { date: '20220801', focus: 0, time: 10, - total: 1000, mergedHosts: [] }, { host: 'www.baidu.com', @@ -14,27 +13,27 @@ test('devide 1', () => { date: '', focus: 0, time: 10, - total: 1000, mergedHosts: [] }] const devided = devide2Buckets(rows) expect(devided.length).toEqual(1) const [bucket, gistData] = devided[0] expect(bucket).toEqual('202208') - expect(gistData).toEqual({ + const expectData: GistData = { "01": { - "www.baidu.com": [10, 0, 1000] + "www.baidu.com": [10, 0] } - }) + } + expect(gistData).toEqual(expectData) }) test('gistData2Rows', () => { const gistData: GistData = { '01': { - 'baidu.com': [0, 1, 2] + 'baidu.com': [0, 1] }, '08': { - 'google.com': [1, 1, 1,] + 'google.com': [1, 1] } } const rows = gistData2Rows('202209', gistData) @@ -45,10 +44,8 @@ test('gistData2Rows', () => { expect(row0.date).toEqual('20220901') expect(row0.time).toEqual(0) expect(row0.focus).toEqual(1) - expect(row0.total).toEqual(2) expect(row1.date).toEqual('20220908') expect(row1.time).toEqual(1) expect(row1.focus).toEqual(1) - expect(row1.total).toEqual(1) }) \ No newline at end of file diff --git a/test/database/timer-database.test.ts b/test/database/timer-database.test.ts index 1e66bba60..9a5c3923f 100644 --- a/test/database/timer-database.test.ts +++ b/test/database/timer-database.test.ts @@ -16,42 +16,42 @@ describe('timer-database', () => { beforeEach(async () => storage.local.clear()) test('1', async () => { - await db.accumulate(baidu, now, resultOf(100, 100, 0)) + await db.accumulate(baidu, now, resultOf(100, 0)) const data: timer.stat.Result = await db.get(baidu, now) - expect(data).toEqual(resultOf(100, 100, 0)) + expect(data).toEqual(resultOf(100, 0)) }) test('2', async () => { - await db.accumulate(baidu, now, resultOf(100, 200, 0)) - await db.accumulate(baidu, now, resultOf(100, 200, 0)) + await db.accumulate(baidu, now, resultOf(200, 0)) + await db.accumulate(baidu, now, resultOf(200, 0)) let data = await db.get(baidu, now) - expect(data).toEqual(resultOf(200, 400, 0)) - await db.accumulate(baidu, now, resultOf(0, 0, 1)) + expect(data).toEqual(resultOf(400, 0)) + await db.accumulate(baidu, now, resultOf(0, 1)) data = await db.get(baidu, now) - expect(data).toEqual(resultOf(200, 400, 1)) + expect(data).toEqual(resultOf(400, 1)) }) test('3', async () => { await db.accumulateBatch( { - [google]: resultOf(11, 11, 0), - [baidu]: resultOf(1, 1, 0) + [google]: resultOf(11, 0), + [baidu]: resultOf(1, 0) }, now ) expect((await db.select()).length).toEqual(2) await db.accumulateBatch( { - [google]: resultOf(12, 12, 1), - [baidu]: resultOf(2, 2, 1) + [google]: resultOf(12, 1), + [baidu]: resultOf(2, 1) }, yesterday ) expect((await db.select()).length).toEqual(4) await db.accumulateBatch( { - [google]: resultOf(13, 13, 2), - [baidu]: resultOf(3, 3, 2) + [google]: resultOf(13, 2), + [baidu]: resultOf(3, 2) }, beforeYesterday ) expect((await db.select()).length).toEqual(6) @@ -72,9 +72,9 @@ describe('timer-database', () => { // By daterange cond = {} cond.date = [now, now] - const expectedResult = [ - { date: nowStr, focus: 11, host: google, mergedHosts: [], time: 0, total: 11 }, - { date: nowStr, focus: 1, host: baidu, mergedHosts: [], time: 0, total: 1 } + const expectedResult: timer.stat.Row[] = [ + { date: nowStr, focus: 11, host: google, mergedHosts: [], time: 0 }, + { date: nowStr, focus: 1, host: baidu, mergedHosts: [], time: 0 } ] expect(await db.select(cond)).toEqual(expectedResult) // Only use start @@ -90,22 +90,9 @@ describe('timer-database', () => { // test item cond = {} - // No range, all returned - cond.totalRange = [] - expect((await db.select(cond)).length).toEqual(6) - // Only 2 item - cond.totalRange = [, 2] - expect((await db.select(cond)).length).toEqual(2) - // Expect 1 item - cond.totalRange = [2,] - expect((await db.select(cond)).length).toEqual(5) - // focus [0,10] && total [2, unlimited) + // focus [0,10] cond.focusRange = [0, 10] - expect((await db.select(cond)).length).toEqual(2) - - // only focus [0,10] - cond.totalRange = [] expect((await db.select(cond)).length).toEqual(3) // time [2, 3] @@ -115,8 +102,8 @@ describe('timer-database', () => { }) test('5', async () => { - await db.accumulate(baidu, now, resultOf(10, 10, 0)) - await db.accumulate(baidu, yesterday, resultOf(10, 12, 0)) + await db.accumulate(baidu, now, resultOf(10, 0)) + await db.accumulate(baidu, yesterday, resultOf(12, 0)) expect((await db.select()).length).toEqual(2) // Delete yesterday's data await db.deleteByUrlAndDate(baidu, yesterday) @@ -125,8 +112,8 @@ describe('timer-database', () => { await db.deleteByUrlAndDate(baidu, yesterday) expect((await db.get(baidu, now)).focus).toEqual(10) // Add one again, and another - await db.accumulate(baidu, beforeYesterday, resultOf(1, 1, 1)) - await db.accumulate(google, now, resultOf(0, 0, 0)) + await db.accumulate(baidu, beforeYesterday, resultOf(1, 1)) + await db.accumulate(google, now, resultOf(0, 0)) expect((await db.select()).length).toEqual(3) // Delete all the baidu await db.deleteByUrl(baidu) @@ -138,7 +125,7 @@ describe('timer-database', () => { const list = await db.select(cond) expect(list.length).toEqual(1) // Add one item of baidu again again - await db.accumulate(baidu, now, resultOf(1, 1, 1)) + await db.accumulate(baidu, now, resultOf(1, 1)) // But delete google await db.delete(list) // Then only one item of baidu @@ -150,11 +137,11 @@ describe('timer-database', () => { expect((await db.select()).length).toEqual(0) // Return zero instance const result = await db.get(baidu, now) - expect([result.focus, result.time, result.total]).toEqual([0, 0, 0]) + expect([result.focus, result.time]).toEqual([0, 0]) }) test('7', async () => { - const foo = resultOf(1, 1, 1) + const foo = resultOf(1, 1) await db.accumulate(baidu, now, foo) await db.accumulate(baidu, yesterday, foo) await db.accumulate(baidu, beforeYesterday, foo) @@ -166,7 +153,7 @@ describe('timer-database', () => { }) test("importData", async () => { - const foo = resultOf(1, 1, 1) + const foo = resultOf(1, 1) await db.accumulate(baidu, now, foo) const data2Import = await db.storage.get() storage.local.clear() @@ -179,7 +166,6 @@ describe('timer-database', () => { expect(item.date).toEqual(formatTime(now, "{y}{m}{d}")) expect(item.host).toEqual(baidu) expect(item.focus).toEqual(1) - expect(item.total).toEqual(1) expect(item.time).toEqual(1) }) @@ -188,8 +174,7 @@ describe('timer-database', () => { // Valid "20210910github.com": { focus: 1, - time: 1, - total: 1 + time: 1 }, // Valid "20210911github.com": { @@ -216,10 +201,10 @@ describe('timer-database', () => { }) test("count", async () => { - await db.accumulate(baidu, now, resultOf(1, 1, 1)) - await db.accumulate(baidu, yesterday, resultOf(1, 2, 1)) - await db.accumulate(google, now, resultOf(1, 3, 1)) - await db.accumulate(google, yesterday, resultOf(1, 4, 1)) + await db.accumulate(baidu, now, resultOf(1, 1)) + await db.accumulate(baidu, yesterday, resultOf(2, 1)) + await db.accumulate(google, now, resultOf(3, 1)) + await db.accumulate(google, yesterday, resultOf(4, 1)) // Count by host expect(await db.count({ host: baidu, diff --git a/test/util/stat.test.ts b/test/util/stat.test.ts index 655548fd6..9db5dbc39 100644 --- a/test/util/stat.test.ts +++ b/test/util/stat.test.ts @@ -11,8 +11,7 @@ test('default values of WastePerDay', () => { const newOne = createZeroResult() expect(newOne.time).toBe(0) expect(newOne.focus).toBe(0) - expect(newOne.total).toBe(0) - const another = mergeResult(newOne, resultOf(1, 1, 2)) - expect(another).toEqual(resultOf(1, 1, 2)) + const another = mergeResult(newOne, resultOf(1, 2)) + expect(another).toEqual(resultOf(1, 2)) }) \ No newline at end of file From 7cd4485aaf1854967aa925bfde3c6258dbbae28c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 5 Jan 2023 23:46:04 +0800 Subject: [PATCH 074/168] Add crowdin link --- .github/ISSUE_TEMPLATE/translation-------.md | 11 ----------- README-zh.md | 6 +----- README.md | 2 +- 3 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/translation-------.md diff --git a/.github/ISSUE_TEMPLATE/translation-------.md b/.github/ISSUE_TEMPLATE/translation-------.md deleted file mode 100644 index 1cc7fd8ff..000000000 --- a/.github/ISSUE_TEMPLATE/translation-------.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Translation Issue / 翻译改善建议 -about: Improve the translation of this extension -title: 'Report translation mistakes' -labels: 'locale' -assignees: '' ---- - -## Any Snapshot? - - diff --git a/README-zh.md b/README-zh.md index c25de261d..eafb64902 100644 --- a/README-zh.md +++ b/README-zh.md @@ -50,10 +50,6 @@ ---- - -详细展示图文:[douban.com](https://www.douban.com/group/topic/213888429/) - ## 贡献指南 如果你想参与到该项目的开源建设,可以考虑以下几种方式 @@ -72,7 +68,7 @@ #### 完善翻译 -除了简体中文外,该扩展另外的本地化语言都依赖机翻。所以也非常欢迎您在 [issue](https://github.com/sheepzh/timer/issues/new?assignees=&labels=locale&template=translation-------.md&title=Report+translation+mistakes) 里提交翻译建议。 +除了简体中文外,该扩展另外的本地化语言都依赖机翻。所以也非常欢迎您在 [Crowdin](https://crowdin.com/project/timer-chrome-edge-firefox) 里提交翻译建议。 #### 好评鼓励 diff --git a/README.md b/README.md index 1416e381c..947320ca8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you know how to develop browser extensions and are familiar with the project' #### 3. Perfect translation -In addition to Simplified Chinese, the other localized languages of this software all rely on machine translation. You can also submit translation suggestions with [issues](https://github.com/sheepzh/timer/issues/new?assignees=&labels=locale&template=translation-------.md&title=Report+translation+mistakes). +In addition to Simplified Chinese, the other localized languages of this software all rely on machine translation. You can also submit translation suggestions on [Crowdin](https://crowdin.com/project/timer-chrome-edge-firefox). #### 4. Rate 5 stars From 54812514a1033e4878f2c1722efc3100e56283ba Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 8 Jan 2023 19:13:35 +0800 Subject: [PATCH 075/168] Fix sub menu item of guide page --- src/guide/component/common.ts | 9 +++++---- src/guide/component/privacy.ts | 4 ++-- src/guide/component/usage.ts | 8 ++++---- src/guide/guide.d.ts | 14 ++++++++++++++ src/guide/layout/content.ts | 14 ++++++++------ src/guide/layout/index.ts | 4 ++-- src/guide/layout/menu.ts | 26 ++++++++++++-------------- src/guide/util.ts | 3 +++ 8 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 src/guide/guide.d.ts create mode 100644 src/guide/util.ts diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts index 923831a61..23db0df4c 100644 --- a/src/guide/component/common.ts +++ b/src/guide/component/common.ts @@ -3,13 +3,14 @@ import type { VNode } from "vue" import { t, tN } from "@guide/locale" import { h } from "vue" +import { position2AnchorClz } from "@guide/util" -export function h1(i18nKey: I18nKey, archorClz: string, i18nParam?: any): VNode { - return h('h1', { class: `guide-h1 archor-${archorClz}` }, t(i18nKey, i18nParam)) +export function h1(i18nKey: I18nKey, position: Position, i18nParam?: any): VNode { + return h('h1', { class: `guide-h1 ${position2AnchorClz(position)}` }, t(i18nKey, i18nParam)) } -export function h2(i18nKey: I18nKey, archorClz: string): VNode { - return h('h2', { class: `guide-h2 archor-${archorClz}` }, t(i18nKey)) +export function h2(i18nKey: I18nKey, position: Position): VNode { + return h('h2', { class: `guide-h2 ${position2AnchorClz(position)}` }, t(i18nKey)) } export function paragraph(i18nKey: I18nKey, param?: any): VNode { diff --git a/src/guide/component/privacy.ts b/src/guide/component/privacy.ts index e7355bfe6..c64545097 100644 --- a/src/guide/component/privacy.ts +++ b/src/guide/component/privacy.ts @@ -5,14 +5,14 @@ const _default = defineComponent({ setup() { return () => section( h1(msg => msg.layout.menu.privacy.title, 'privacy'), - h2(msg => msg.layout.menu.privacy.scope, 'scope'), + h2(msg => msg.layout.menu.privacy.scope, 'privacy.scope'), paragraph(msg => msg.privacy.scope.p1), list( msg => msg.privacy.scope.l1, msg => msg.privacy.scope.l2, msg => msg.privacy.scope.l3, ), - h2(msg => msg.layout.menu.privacy.storage, 'storage'), + h2(msg => msg.layout.menu.privacy.storage, 'privacy.storage'), paragraph(msg => msg.privacy.storage.p1), paragraph(msg => msg.privacy.storage.p2), paragraph(msg => msg.privacy.storage.p3), diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts index 0b0748c35..b634e1032 100644 --- a/src/guide/component/usage.ts +++ b/src/guide/component/usage.ts @@ -4,7 +4,7 @@ import { h1, h2, paragraph, list, link, section, linkInner } from "./common" import { t } from "../locale" const quickstart = () => [ - h2(msg => msg.layout.menu.usage.quickstart, 'quickstart'), + h2(msg => msg.layout.menu.usage.quickstart, 'usage.quickstart'), paragraph(msg => msg.usage.quickstart.p1), list( msg => msg.usage.quickstart.l1, @@ -17,7 +17,7 @@ const quickstart = () => [ const backgroundPageUrl = getAppPageUrl(false) const background = () => [ - h2(msg => msg.layout.menu.usage.background, 'background'), + h2(msg => msg.layout.menu.usage.background, 'usage.background'), paragraph(msg => msg.usage.background.p1, { background: linkInner(backgroundPageUrl, t(msg => msg.usage.background.backgroundPage)) }), @@ -29,7 +29,7 @@ const background = () => [ ] const advanced = () => [ - h2(msg => msg.layout.menu.usage.advanced, 'advanced'), + h2(msg => msg.layout.menu.usage.advanced, 'usage.advanced'), paragraph(msg => msg.usage.advanced.p1), list( msg => msg.usage.advanced.l1, @@ -46,7 +46,7 @@ const advanced = () => [ ] const backup = () => [ - h2(msg => msg.layout.menu.usage.backup, 'backup'), + h2(msg => msg.layout.menu.usage.backup, 'usage.backup'), paragraph(msg => msg.usage.backup.p1, { gist: link('https://gist.github.com/', 'Github Gist') }), list( [msg => msg.usage.backup.l1, { diff --git a/src/guide/guide.d.ts b/src/guide/guide.d.ts new file mode 100644 index 000000000..a14023b1c --- /dev/null +++ b/src/guide/guide.d.ts @@ -0,0 +1,14 @@ + +/** + * The anchor of menu items + */ +declare type Position = + | 'profile' + | 'usage' + | 'usage.quickstart' + | 'usage.background' + | 'usage.advanced' + | 'usage.backup' + | 'privacy' + | 'privacy.scope' + | 'privacy.storage' \ No newline at end of file diff --git a/src/guide/layout/content.ts b/src/guide/layout/content.ts index 1343f4e90..5a9e0c944 100644 --- a/src/guide/layout/content.ts +++ b/src/guide/layout/content.ts @@ -1,25 +1,27 @@ import { ElDivider } from "element-plus" -import { watch, defineComponent, h, onMounted } from "vue" +import { watch, defineComponent, h, onMounted, PropType } from "vue" import Profile from "../component/profile" import Usage from "../component/usage" import Privacy from "../component/privacy" +import { position2AnchorClz } from "@guide/util" -function scrolePosition(position: string) { - document.querySelector(`.archor-${position}`)?.scrollIntoView?.() +function scrollPosition(position: Position) { + console.log(position) + document.querySelector(`.${position2AnchorClz(position)}`)?.scrollIntoView?.() } const _default = defineComponent({ name: 'GuideContent', props: { position: { - type: String, + type: String as PropType, required: false, } }, setup(props) { - onMounted(() => scrolePosition(props.position)) - watch(() => props.position, newVal => newVal && scrolePosition(newVal)) + onMounted(() => scrollPosition(props.position)) + watch(() => props.position, newVal => newVal && scrollPosition(newVal)) return () => [ h(Profile), h(ElDivider), diff --git a/src/guide/layout/index.ts b/src/guide/layout/index.ts index ed38ee602..7cec2f643 100644 --- a/src/guide/layout/index.ts +++ b/src/guide/layout/index.ts @@ -15,9 +15,9 @@ import Menu from "./menu" const _default = defineComponent({ name: "Guide", render() { - const position: Ref = ref() + const position: Ref = ref() return h(ElContainer, { class: 'guide-container' }, () => [ - h(ElAside, {}, () => h(Menu, { onClick: (newPosition: string) => position.value = newPosition })), + h(ElAside, {}, () => h(Menu, { onClick: (newPosition: Position) => position.value = newPosition })), h(ElContainer, { id: 'app-body' }, () => h(ElMain, {}, () => h(Content, { position: position.value }))) diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 7fea3c936..726b65c62 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -6,7 +6,6 @@ */ import { ref, VNode } from "vue" - import ElementIcon from "@src/element-ui/icon" import { I18nKey, t } from "@guide/locale" import { ElIcon, ElMenu, ElMenuItem, ElSubMenu } from "element-plus" @@ -16,18 +15,18 @@ import { User, Memo, MagicStick } from "@element-plus/icons-vue" type _Item = { title: I18nKey - position: string + position: Position } type _Group = { title: I18nKey - position: string + position: Position children: _Item[] icon: ElementIcon } -const quickstartPosition = 'quickstart' -const profilePosition = 'profile' +const quickstartPosition: Position = 'usage.quickstart' +const profilePosition: Position = 'profile' const menus: _Group[] = [ { title: msg => msg.layout.menu.usage.title, @@ -38,13 +37,13 @@ const menus: _Group[] = [ position: quickstartPosition }, { title: msg => msg.layout.menu.usage.background, - position: 'background' + position: 'usage.background' }, { title: msg => msg.layout.menu.usage.advanced, - position: 'advanced' + position: 'usage.advanced' }, { title: msg => msg.layout.menu.usage.backup, - position: 'backup', + position: 'usage.backup', } ], icon: Memo @@ -56,18 +55,18 @@ const menus: _Group[] = [ children: [ { title: msg => msg.layout.menu.privacy.scope, - position: 'scope' + position: 'privacy.scope' }, { title: msg => msg.layout.menu.privacy.storage, - position: 'storage' + position: 'privacy.storage' }, ], } ] -function renderMenuItem(handleClick: (position: string) => void, item: _Item, index: number): VNode { +function renderMenuItem(handleClick: (position: Position) => void, item: _Item, index: number): VNode { const { title, position } = item return h(ElMenuItem, { index: position, @@ -75,11 +74,10 @@ function renderMenuItem(handleClick: (position: string) => void, item: _Item, in }, () => h('span', {}, `${index + 1}. ${t(title)}`)) } -function renderGroup(handleClick: (position: string) => void, group: _Group): VNode { +function renderGroup(handleClick: (position: Position) => void, group: _Group): VNode { const { position, title, children, icon } = group return h(ElSubMenu, { index: position, - onClick: () => handleClick(position) }, { title: () => [ h(ElIcon, () => h(icon)), @@ -95,7 +93,7 @@ const _default = defineComponent({ name: "GuideMenu", emits: ['click'], setup(_, ctx) { - const handleClick = (position: string) => ctx.emit('click', position) + const handleClick = (position: Position) => ctx.emit('click', position) const menuItems = () => [ h(ElMenuItem, { index: profilePosition, diff --git a/src/guide/util.ts b/src/guide/util.ts new file mode 100644 index 000000000..474f93566 --- /dev/null +++ b/src/guide/util.ts @@ -0,0 +1,3 @@ +export function position2AnchorClz(position: Position): string { + return `anchor-${position?.replace('.', '-')}` +} \ No newline at end of file From 72aa2a3e57cc09e86872e1db481720a70f321eae Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 9 Jan 2023 20:37:39 +0800 Subject: [PATCH 076/168] Support to modify the daily time limit (#176) --- .../dashboard/components/top-k-visit.ts | 4 +- src/app/components/limit/index.ts | 5 +- .../components/limit/modify/form/common.ts | 16 ++ .../components/limit/modify/form/form.d.ts | 41 +++++ src/app/components/limit/modify/form/index.ts | 144 +++++++++++------- .../components/limit/modify/form/path-edit.ts | 75 ++++++--- .../limit/modify/form/time-limit.ts | 97 ++++++++---- .../limit/modify/form/url-path-item.ts | 22 --- src/app/components/limit/modify/form/url.ts | 134 +++++++++------- src/app/components/limit/modify/index.ts | 34 ++++- src/app/components/limit/modify/modify.d.ts | 1 + .../limit/table/column/operation.ts | 15 +- src/app/components/limit/table/index.ts | 5 +- src/app/components/report/index.ts | 4 +- src/app/components/trend/index.ts | 4 +- src/database/limit-database.ts | 20 ++- src/i18n/message/app/limit.ts | 5 + src/popup/components/footer/index.ts | 4 +- src/service/timer-service.ts | 9 +- 19 files changed, 413 insertions(+), 226 deletions(-) create mode 100644 src/app/components/limit/modify/form/common.ts create mode 100644 src/app/components/limit/modify/form/form.d.ts delete mode 100644 src/app/components/limit/modify/form/url-path-item.ts create mode 100644 src/app/components/limit/modify/modify.d.ts diff --git a/src/app/components/dashboard/components/top-k-visit.ts b/src/app/components/dashboard/components/top-k-visit.ts index e4604bdd1..c8dffdb76 100644 --- a/src/app/components/dashboard/components/top-k-visit.ts +++ b/src/app/components/dashboard/components/top-k-visit.ts @@ -18,7 +18,7 @@ import TooltipComponent from "@echarts/component/tooltip" use([PieChart, TitleComponent, TooltipComponent]) -import timerService, { SortDirect } from "@service/timer-service" +import timerService from "@service/timer-service" import { MILL_PER_DAY } from "@util/time" import { ElLoading } from "element-plus" import { defineComponent, h, onMounted, ref } from "vue" @@ -113,7 +113,7 @@ const _default = defineComponent({ const query: TimerQueryParam = { date: [startTime, now], sort: "time", - sortOrder: SortDirect.DESC, + sortOrder: 'DESC', mergeDate: true, } const top: timer.stat.Row[] = (await timerService.selectByPage(query, { num: 1, size: TOP_NUM }, { alias: true })).list diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index 52074f917..1e041dd13 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -43,7 +43,7 @@ const _default = defineComponent({ onlyEnabled.value = option.onlyEnabled queryData() }, - onCreate: () => modify.value?.show?.() + onCreate: () => modify.value?.create?.() }), content: () => [ h(LimitTable, { @@ -54,6 +54,9 @@ const _default = defineComponent({ await limitService.remove(row) ElMessage.success(t(msg => msg.limit.message.deleted)) queryData() + }, + async onModify(row: timer.limit.Item) { + modify.value?.modify?.(row) } }), h(Modify, { diff --git a/src/app/components/limit/modify/form/common.ts b/src/app/components/limit/modify/form/common.ts new file mode 100644 index 000000000..ab5233f5e --- /dev/null +++ b/src/app/components/limit/modify/form/common.ts @@ -0,0 +1,16 @@ +export function parseUrl(url: string): UrlInfo { + let protocol: Protocol = '*://' + + url = decodeURI(url)?.trim() + if (url.startsWith('http://')) { + protocol = 'http://' + url = url.substring(protocol.length) + } else if (url.startsWith('https://')) { + protocol = 'https://' + url = url.substring(protocol.length) + } else if (url.startsWith('*://')) { + protocol = '*://' + url = url.substring(protocol.length) + } + return { protocol, url } +} \ No newline at end of file diff --git a/src/app/components/limit/modify/form/form.d.ts b/src/app/components/limit/modify/form/form.d.ts new file mode 100644 index 000000000..24f864764 --- /dev/null +++ b/src/app/components/limit/modify/form/form.d.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +declare type FormInfo = { + /** + * Time / seconds + */ + timeLimit: number + /** + * Protocol + path + */ + condition: string +} + +declare type UrlPart = { + /** + * The origin part text + */ + origin: string + /** + * Whether to replace with wildcard + */ + ignored: boolean +} + +declare type UrlInfo = { + protocol: Protocol + url: string +} + +/** + * The protocol of rule host + */ +declare type Protocol = + | 'http://' + | 'https://' + | '*://' diff --git a/src/app/components/limit/modify/form/index.ts b/src/app/components/limit/modify/form/index.ts index 49bf633c0..2315bf3b4 100644 --- a/src/app/components/limit/modify/form/index.ts +++ b/src/app/components/limit/modify/form/index.ts @@ -6,74 +6,102 @@ */ import { ElForm } from "element-plus" -import { computed, ComputedRef, defineComponent, h, ref, Ref, SetupContext } from "vue" +import { computed, ComputedRef, defineComponent, h, reactive, ref, Ref, UnwrapRef } from "vue" import '../style/el-input' -import pathEdit from "./path-edit" -import timeFormItem from "./time-limit" -import urlFormItem, { FormUrlProps, Protocol } from "./url" -import UrlPathItem from "./url-path-item" +import LimitPathEdit from "./path-edit" +import LimitUrlFormItem from "./url" +import LimitTimeFormItem from "./time-limit" +import { parseUrl } from "./common" -// Limited time -const hourRef: Ref = ref() -const minuteRef: Ref = ref() -const secondRef: Ref = ref() - -// Limited url -const protocolRef: Ref = ref() -const pathItemsRef: Ref = ref([]) -const urlRef: ComputedRef = computed(() => pathItemsRef.value.map(i => i.toString()).join('/')) +function computeFormInfo(urlInfo: UrlInfo, timeRef: Ref): FormInfo { + const { url, protocol } = urlInfo + const result: FormInfo = { + timeLimit: timeRef.value || 0, + condition: url ? protocol + url : '' + } + return result +} -const timeProps = { hourRef, minuteRef, secondRef } -const urlProps: FormUrlProps = { protocolRef, pathItemsRef, urlRef } -const pathEditProps = { pathItemsRef } -const render = () => h(ElForm, - { labelWidth: '100px' }, - () => [timeFormItem(timeProps), urlFormItem(urlProps), pathEdit(pathEditProps)] -) +function init(timeRef: Ref, urlInfo: UrlInfo) { + // 1 hour + timeRef.value = 3600 + urlInfo.protocol = '*://' + urlInfo.url = '' +} -function init() { - hourRef.value = 1 - minuteRef.value = undefined - secondRef.value = undefined - protocolRef.value = Protocol.ALL - pathItemsRef.value = [] +function parseRow(row: timer.limit.Item, timeRef: Ref, urlInfo: UrlInfo) { + const { cond, time } = row + timeRef.value = time || 0 + const { protocol, url } = parseUrl(cond) + urlInfo.url = url + urlInfo.protocol = protocol } -init() +const _default = defineComponent({ + name: "LimitForm", + setup(_, ctx) { + // Limited time + const timeRef: Ref = ref() + // Limited url + const urlInfo: UnwrapRef = reactive({ + protocol: undefined, + url: undefined + }) + const editMode: Ref = ref('create') + init(timeRef, urlInfo) -export type FormData = { - /** - * Time / seconds - */ - timeLimit: number - /** - * Protocol + url - */ - url: string -} + const formInfo: ComputedRef = computed(() => computeFormInfo(urlInfo, timeRef)) + const canEditUrl: ComputedRef = computed(() => editMode.value === 'create') + const pathEditRef: Ref = ref() -const handleGetData = () => { - let timeLimit = 0 - timeLimit += (hourRef.value || 0) * 3600 - timeLimit += (minuteRef.value || 0) * 60 - timeLimit += (secondRef.value || 0) - const url = urlRef.value || '' - const protocol = protocolRef.value - const result: FormData = { - timeLimit, - url: url ? protocol + url : '' - } - return result -} + function setUrl(newUrl: string) { + urlInfo.url = newUrl + pathEditRef?.value?.forceUpdateUrl?.(newUrl) + } -const exposeOption = { - getData: () => handleGetData(), - clean: () => init() -} + ctx.expose({ + getData: () => formInfo.value, + clean: () => { + editMode.value = 'create' + init(timeRef, urlInfo) + }, + modify: (row: timer.limit.Item) => { + editMode.value = 'modify' + parseRow(row, timeRef, urlInfo) + setUrl(urlInfo.url) + }, + }) -const _default = defineComponent({ - setup: (_, context: SetupContext) => context.expose(exposeOption), - render + return () => h(ElForm, + { labelWidth: 120 }, + () => { + const items = [ + h(LimitTimeFormItem, { + modelValue: timeRef.value, + onChange: (newVal: number) => timeRef.value = newVal + }), + h(LimitUrlFormItem, { + url: urlInfo.url, + protocol: urlInfo.protocol, + disabled: !canEditUrl.value, + onUrlChange: (newUrl: string) => setUrl(newUrl), + onProtocolChange: (newProtocol: Protocol) => urlInfo.protocol = newProtocol + }), + ] + canEditUrl.value && items.push( + h(LimitPathEdit, { + ref: pathEditRef, + disabled: editMode.value === 'modify', + url: urlInfo.url, + onUrlChange: (newVal: string) => { + urlInfo.url = newVal + } + }) + ) + return items + } + ) + } }) export default _default \ No newline at end of file diff --git a/src/app/components/limit/modify/form/path-edit.ts b/src/app/components/limit/modify/form/path-edit.ts index b71f3e182..50711af20 100644 --- a/src/app/components/limit/modify/form/path-edit.ts +++ b/src/app/components/limit/modify/form/path-edit.ts @@ -6,20 +6,19 @@ */ import { ElSwitch, ElTag, ElTooltip } from "element-plus" -import { h, Ref, VNode } from "vue" +import { defineComponent, h, ref, Ref, VNode } from "vue" import { t } from "@app/locale" -import UrlPathItem from "./url-path-item" -type _Props = { - pathItemsRef: Ref -} +const switchStyle: Partial = { marginRight: '2px' } -const switchStyle = { marginRight: '2px' } -const tagContent = (item: UrlPathItem, index: number, arr: UrlPathItem[]) => { +const tagContent = (item: UrlPart, index: number, callback: Function) => { const result: VNode[] = [] if (!!index) { const modelValue = item.ignored - const onChange = (val: boolean) => item.ignored = val + const onChange = (val: boolean) => { + item.ignored = val + callback?.() + } const switchNode = h(ElSwitch, { style: switchStyle, modelValue, onChange }) const tooltipNode = h(ElTooltip, { content: t(msg => msg.limit.useWildcard) }, { default: () => switchNode }) @@ -29,20 +28,22 @@ const tagContent = (item: UrlPathItem, index: number, arr: UrlPathItem[]) => { return result } -const tabStyle = { marginBottom: '5px' } -const item2Tag = (item: UrlPathItem, index: number, arr: UrlPathItem[]) => { +const tabStyle: Partial = { + marginBottom: '5px', + marginRight: '0', +} + +const item2Tag = (item: UrlPart, index: number, arr: UrlPart[], onChange: Function) => { const isNotHost: boolean = !!index - return h(ElTag, - { - type: isNotHost ? '' : 'info', - closable: isNotHost, - onClose: () => arr.splice(index), - style: tabStyle + return h(ElTag, { + type: isNotHost ? '' : 'info', + closable: isNotHost, + onClose: () => { + arr.splice(index) + onChange?.() }, - { - default: () => tagContent(item, index, arr) - } - ) + style: tabStyle + }, () => tagContent(item, index, onChange)) } const combineStyle = { fontSize: '14px', @@ -55,6 +56,38 @@ const combineTags = (arr: VNode[], current: VNode) => { return arr } -const _default = (props: _Props) => h('div', {}, props.pathItemsRef.value.map(item2Tag).reduce(combineTags, [])) +function url2PathItems(url: string): UrlPart[] { + return url.split('/').filter(path => path).map(path => ({ origin: path, ignored: path === '*' })) +} + +function pathItems2Url(pathItems: UrlPart[]): string { + return pathItems.map(i => i.ignored ? '*' : i.origin || '').join('/') +} + +const _default = defineComponent({ + name: 'LimitPathEdit', + props: { + url: { + type: String, + required: false, + defaultValue: '' + }, + }, + emits: ['urlChange'], + setup(props, ctx) { + const url = props.url + const items: Ref = ref(url2PathItems(url)) + ctx.expose({ + forceUpdateUrl(url: string) { + items.value = url2PathItems(url) + } + }) + const handleUrlChange = () => ctx.emit('urlChange', pathItems2Url(items.value)) + return () => h('div', {}, items.value + .map((item, index, arr) => item2Tag(item, index, arr, handleUrlChange)) + .reduce(combineTags, []) + ) + } +}) export default _default \ No newline at end of file diff --git a/src/app/components/limit/modify/form/time-limit.ts b/src/app/components/limit/modify/form/time-limit.ts index 5e4511bb3..88bc67d2f 100644 --- a/src/app/components/limit/modify/form/time-limit.ts +++ b/src/app/components/limit/modify/form/time-limit.ts @@ -5,51 +5,84 @@ * https://opensource.org/licenses/MIT */ +import type { ComputedRef, Ref } from "vue" + import { ElCol, ElFormItem, ElInput, ElRow } from "element-plus" -import { Ref, h } from "vue" +import { defineComponent, h, computed, ref, watch } from "vue" import { t } from "@app/locale" -type _Props = { - hourRef: Ref - minuteRef: Ref - secondRef: Ref -} - -const handleInput = (val: string, ref: Ref, maxVal: number) => { - val = val.trim() - if (!val) { +const handleInput = (inputVal: string, ref: Ref, maxVal: number) => { + inputVal = inputVal?.trim?.() + if (!inputVal) { ref.value = undefined return } - let num = Number.parseInt(val) + let num = Number.parseInt(inputVal) if (isNaN(num)) return if (num < 0) num = 0 if (num > maxVal) num = maxVal ref.value = num } -const timeInput = (valRef: Ref, unit: string, maxVal: number) => h(ElCol, { span: 8 }, - () => h(ElInput, - { - modelValue: valRef.value === undefined ? '' : valRef.value.toString(), - clearable: true, - onInput: (val: string) => handleInput(val, valRef, maxVal), - onClear: () => valRef.value = undefined, - placeholder: '0', - class: 'limit-modify-time-limit-input' - }, - { - append: () => unit - } - ) +const timeInput = (ref: Ref, unit: string, maxVal: number) => h(ElCol, { span: 8 }, + () => h(ElInput, { + modelValue: ref.value, + clearable: true, + onInput: (val: string) => handleInput(val, ref, maxVal), + onClear: () => ref.value = undefined, + placeholder: '0', + class: 'limit-modify-time-limit-input' + }, { + append: () => unit + }) ) -const timeInputCols = (props: _Props) => h(ElRow, { gutter: 10 }, () => [ - timeInput(props.hourRef, 'H', 23), - timeInput(props.minuteRef, 'M', 59), - timeInput(props.secondRef, 'S', 59) -]) +function computeSecond2LimitInfo(time: number): [number, number, number] { + time = time || 0 + const second = time % 60 + const totalMinutes = (time - second) / 60 + const minute = totalMinutes % 60 + const hour = (totalMinutes - minute) / 60 + return [hour, minute, second] +} -const timeFormItem = (props: _Props) => h(ElFormItem, { label: t(msg => msg.limit.item.time) }, () => timeInputCols(props)) +function computeLimitInfo2Second(hourRef: Ref, minuteRef: Ref, secondRef: Ref): number { + let time = 0 + time += (hourRef.value || 0) * 3600 + time += (minuteRef.value || 0) * 60 + time += (secondRef.value || 0) + return time +} + +const _default = defineComponent({ + name: "LimitTimeLimit", + props: { + modelValue: { + type: Number + } + }, + emits: ['change'], + setup(props, ctx) { + const [hour, minute, second] = computeSecond2LimitInfo(props.modelValue) + const hourRef: Ref = ref(hour) + const minuteRef: Ref = ref(minute) + const secondRef: Ref = ref(second) + watch(() => props.modelValue, (newVal: number) => { + const [hour, minute, second] = computeSecond2LimitInfo(newVal) + hourRef.value = hour + minuteRef.value = minute + secondRef.value = second + }) + const limitTime: ComputedRef = computed(() => computeLimitInfo2Second(hourRef, minuteRef, secondRef)) + watch([hourRef, minuteRef, secondRef], () => ctx.emit('change', limitTime.value)) + return () => h(ElFormItem, { + label: t(msg => msg.limit.item.time) + }, () => h(ElRow, { gutter: 10 }, () => [ + timeInput(hourRef, 'H', 23), + timeInput(minuteRef, 'M', 59), + timeInput(secondRef, 'S', 59) + ])) + } +}) -export default timeFormItem \ No newline at end of file +export default _default \ No newline at end of file diff --git a/src/app/components/limit/modify/form/url-path-item.ts b/src/app/components/limit/modify/form/url-path-item.ts deleted file mode 100644 index 011e83cd7..000000000 --- a/src/app/components/limit/modify/form/url-path-item.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -export default class UrlPathItem { - origin: string - ignored: boolean - - static of(origin: string, ignored?: boolean) { - const item: UrlPathItem = new UrlPathItem() - item.origin = origin - item.ignored = !!ignored - return item - } - - toString(): string { - return this.ignored ? "*" : (this.origin || '') - } -} \ No newline at end of file diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index fc7eb59d6..4f33a92de 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -5,49 +5,37 @@ * https://opensource.org/licenses/MIT */ -import { h, Ref } from "vue" +import { defineComponent, h, PropType, ref, Ref, watch } from "vue" import clipboardy from "clipboardy" import { t } from "@app/locale" import { ElButton, ElFormItem, ElInput, ElOption, ElSelect } from "element-plus" -import UrlPathItem from "./url-path-item" import { checkPermission, requestPermission } from "@src/permissions" import { IS_FIREFOX } from "@util/constant/environment" +import { parseUrl } from "./common" -export enum Protocol { - HTTP = 'http://', - HTTPS = 'https://', - ALL = '*://' -} +const ALL_PROTOCOLS: Protocol[] = ['http://', 'https://', '*://'] -type _Props = { - protocolRef: Ref - pathItemsRef: Ref - urlRef: Ref +export function computeUrl(pathItems: UrlPart[]): string { + return pathItems.map(i => i.ignored ? '*' : i.origin || '').join('/') } -export type FormUrlProps = _Props - -const protocolOptions = () => Object.entries(Protocol) - .map(([_name, value]) => value) - .map(prefix => h(ElOption, { value: prefix, label: prefix })) - -const protocolSelect = (protocolRef: Ref) => h(ElSelect, { - modelValue: protocolRef.value, - onChange: (val: string) => protocolRef.value = val -}, protocolOptions) +const protocolOptions = () => ALL_PROTOCOLS.map(prefix => h(ElOption, { value: prefix, label: prefix })) -const url2PathItems = (url: string) => { +function cleanUrl(url: string): string { + if (!url) { + return '' + } const querySign = url.indexOf('?') querySign > -1 && (url = url.substring(0, querySign)) const hashSign = url.indexOf('#') hashSign > -1 && (url = url.substring(0, hashSign)) - return url.split('/').filter(path => path).map(path => UrlPathItem.of(path)) + return url } const PERMISSION = 'clipboardRead' const FIREFOX_NO_PERMISSION_MSG = t(msg => msg.limit.message.noPermissionFirefox) -const handlePaste = async (protocolRef: Ref, pathItemsRef: Ref) => { +async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler: (newProtocol: Protocol) => void) { let granted = await checkPermission(PERMISSION) if (!granted) { @@ -67,46 +55,74 @@ const handlePaste = async (protocolRef: Ref, pathItemsRef: Ref msg.limit.button.paste) -const urlPaste = (protocolRef: Ref, pathItemsRef: Ref) => h(ElButton, - { - onClick: () => handlePaste(protocolRef, pathItemsRef) - }, - () => pasteButtonText -) const placeholder = t(msg => msg.limit.urlPlaceholder) -const urlInput = ({ protocolRef, urlRef, pathItemsRef }: _Props) => h(ElInput, - { - modelValue: urlRef.value, - clearable: true, - onClear: () => pathItemsRef.value = [], - // Disabled this input in the css to customized the styles - // @see ../style/el-input.sass - // @see this.onInput - // disabled: true, - onInput: (_val: string) => { /** Do nothing */ }, - placeholder +const _default = defineComponent({ + name: 'LimitUrlFormItem', + emits: ['urlChange', 'protocolChange'], + props: { + url: String, + protocol: String as PropType, + disabled: { + type: Boolean, + defaultValue: false + } }, - { - prefix: () => protocolSelect(protocolRef), - append: () => urlPaste(protocolRef, pathItemsRef) + setup(props, ctx) { + // protocol + const protocolRef: Ref = ref(props.protocol) + watch(() => props.protocol, () => protocolRef.value = props.protocol) + watch(protocolRef, () => ctx.emit('protocolChange', protocolRef.value)) + // url + const urlRef: Ref = ref(props.url) + watch(urlRef, () => ctx.emit('urlChange', urlRef.value)) + watch(() => props.url, () => urlRef.value = props.url) + + return () => h(ElFormItem, { label: t(msg => msg.limit.item.condition) }, + () => { + const slots: any = { + prefix: () => h(ElSelect, { + modelValue: protocolRef.value, + onChange: (val: string) => protocolRef.value = val as Protocol, + disabled: props.disabled, + }, protocolOptions), + } + !props.disabled && (slots.append = () => h(ElButton, { + onClick: () => handlePaste( + url => { + urlRef.value = url + ctx.emit('urlChange', url) + }, + prot => protocolRef.value = prot + ) + }, () => pasteButtonText)) + + return h(ElInput, { + modelValue: urlRef.value, + clearable: !props.disabled, + disabled: props.disabled, + onClear() { + urlRef.value = '' + ctx.emit('urlChange', '') + }, + // Disabled this input in the css to customized the styles + // @see ../style/el-input.sass + // @see this.onInput + // disabled: true, + onInput: (_val: string) => { /** Do nothing */ }, + placeholder + }, slots) + } + ) } -) -const urlFormItem = (props: _Props) => h(ElFormItem, { label: t(msg => msg.limit.item.condition) }, () => urlInput(props)) +}) -export default urlFormItem \ No newline at end of file +export default _default \ No newline at end of file diff --git a/src/app/components/limit/modify/index.ts b/src/app/components/limit/modify/index.ts index f2c8ac6e3..e142d2d80 100644 --- a/src/app/components/limit/modify/index.ts +++ b/src/app/components/limit/modify/index.ts @@ -6,8 +6,8 @@ */ import { ElDialog, ElMessage } from "element-plus" -import { defineComponent, h, ref, Ref } from "vue" -import Form, { FormData } from "./form" +import { defineComponent, h, nextTick, ref, Ref } from "vue" +import Form from "./form" import Footer from "./footer" import LimitDatabase from "@db/limit-database" import { t } from "@app/locale" @@ -21,9 +21,23 @@ const _default = defineComponent({ setup: (_, ctx) => { const visible: Ref = ref(false) const form: Ref = ref() + const mode: Ref = ref() + // Cache + let modifyingItem: timer.limit.Item = undefined ctx.expose({ - show: () => visible.value = true, + create() { + visible.value = true + mode.value = 'create' + modifyingItem = undefined + nextTick(() => form.value?.clean?.()) + }, + modify(row: timer.limit.Item) { + visible.value = true + mode.value = 'modify' + modifyingItem = { ...row } + nextTick(() => form.value?.modify?.(row)) + }, hide: () => visible.value = false }) @@ -36,8 +50,8 @@ const _default = defineComponent({ default: () => h(Form, { ref: form }), footer: () => h(Footer, { async onSave() { - const { url, timeLimit }: FormData = form.value?.getData?.() - if (!url) { + const { condition, timeLimit }: FormInfo = form.value?.getData?.() + if (!condition) { ElMessage.warning(noUrlError) return } @@ -45,12 +59,16 @@ const _default = defineComponent({ ElMessage.warning(noTimeError) return } - const toInsert: timer.limit.Rule = { cond: url, time: timeLimit, enabled: true, allowDelay: true } - await db.save(toInsert) + const toSave: timer.limit.Rule = { cond: condition, time: timeLimit, enabled: true, allowDelay: true } + if (mode.value === 'modify' && modifyingItem) { + toSave.enabled = modifyingItem.enabled + toSave.allowDelay = modifyingItem.allowDelay + } + await db.save(toSave, mode.value === 'modify') visible.value = false ElMessage.success(t(msg => msg.limit.message.saved)) form.value?.clean?.() - ctx.emit("save", toInsert) + ctx.emit("save", toSave) } }) }) diff --git a/src/app/components/limit/modify/modify.d.ts b/src/app/components/limit/modify/modify.d.ts new file mode 100644 index 000000000..a5ece4c0b --- /dev/null +++ b/src/app/components/limit/modify/modify.d.ts @@ -0,0 +1 @@ +declare type Mode = 'modify' | 'create' \ No newline at end of file diff --git a/src/app/components/limit/table/column/operation.ts b/src/app/components/limit/table/column/operation.ts index efbade6aa..1fec22119 100644 --- a/src/app/components/limit/table/column/operation.ts +++ b/src/app/components/limit/table/column/operation.ts @@ -5,21 +5,22 @@ * https://opensource.org/licenses/MIT */ -import { Delete } from "@element-plus/icons-vue" +import { Delete, Edit } from "@element-plus/icons-vue" import { ElButton, ElMessageBox, ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" const label = t(msg => msg.limit.item.operation) const deleteButtonText = t(msg => msg.limit.button.delete) +const modifyButtonText = t(msg => msg.limit.button.modify) const _default = defineComponent({ name: "LimitOperationColumn", - emits: ["rowDelete"], + emits: ["rowDelete", "rowModify"], setup(_props, ctx) { return () => h(ElTableColumn, { prop: 'operations', label, - minWidth: 80, + minWidth: 200, align: 'center', }, { default: ({ row }: { row: timer.limit.Item }) => [ @@ -34,7 +35,13 @@ const _default = defineComponent({ .then(() => ctx.emit("rowDelete", row, cond)) .catch(() => { /** Do nothing */ }) } - }, () => deleteButtonText) + }, () => deleteButtonText), + h(ElButton, { + type: 'primary', + size: 'small', + icon: Edit, + onClick: () => ctx.emit('rowModify', row), + }, () => modifyButtonText) ] }) } diff --git a/src/app/components/limit/table/index.ts b/src/app/components/limit/table/index.ts index 8b3a88346..8ad1c1250 100644 --- a/src/app/components/limit/table/index.ts +++ b/src/app/components/limit/table/index.ts @@ -19,7 +19,7 @@ const _default = defineComponent({ props: { data: Array as PropType }, - emits: ["delayChange", "enabledChange", "delete"], + emits: ["delayChange", "enabledChange", "delete", "modify"], setup(props, ctx) { return () => h(ElTable, { border: true, @@ -39,7 +39,8 @@ const _default = defineComponent({ onRowChange: (row: timer.limit.Item, _enabled: boolean) => ctx.emit("enabledChange", row) }), h(LimitOperationColumn, { - onRowDelete: (row: timer.limit.Item, _cond: string) => ctx.emit("delete", row) + onRowDelete: (row: timer.limit.Item, _cond: string) => ctx.emit("delete", row), + onRowModify: (row: timer.limit.Item) => ctx.emit("modify", row), }) ]) } diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index d5d7b3d74..ce373b8b5 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -13,7 +13,7 @@ import type { Router, RouteLocation } from "vue-router" import { computed, defineComponent, h, reactive, ref } from "vue" import { I18nKey, t } from "@app/locale" -import timerService, { SortDirect } from "@service/timer-service" +import timerService from "@service/timer-service" import whitelistService from "@service/whitelist-service" import './styles/element' import ReportTable, { ElSortDirect } from "./table" @@ -197,7 +197,7 @@ function computeTimerQueryParam(filterOption: timer.app.report.FilterOption, sor mergeHost: filterOption.mergeHost, mergeDate: filterOption.mergeDate, sort: sort.prop, - sortOrder: sort.order === ElSortDirect.ASC ? SortDirect.ASC : SortDirect.DESC + sortOrder: sort.order === ElSortDirect.ASC ? 'ASC' : 'DESC' } } diff --git a/src/app/components/trend/index.ts b/src/app/components/trend/index.ts index 6984280da..29e21adf1 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, { SortDirect, TimerQueryParam } from "@service/timer-service" +import timerService, { TimerQueryParam } from "@service/timer-service" type _Queries = { host: string @@ -41,7 +41,7 @@ async function query(hostOption: Ref, dateRange: Ref { const items = await this.getItems() - if (!rewrite && items[data.cond]) { - // Not rewrite - return + const { cond, time, enabled, allowDelay } = data + const existItem = items[cond] + if (existItem) { + if (!rewrite) { + // Not rewrite + return + } + // Rewrite + existItem.t = time + existItem.e = enabled + existItem.ad = allowDelay + } else { + // New one + items[cond] = { t: time, e: enabled, ad: allowDelay, w: 0, d: '' } } - items[data.cond] = { t: data.time, e: data.enabled, ad: data.allowDelay, w: 0, d: '' } - this.update(items) + await this.update(items) } async remove(cond: string): Promise { diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index 8d91c659d..e9cd01740 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -27,6 +27,7 @@ export type LimitMessage = { paste: string save: string delete: string + modify: string } timeUnit: { hour: string @@ -62,6 +63,7 @@ const _default: Messages = { paste: '粘贴', save: '保存', delete: '删除', + modify: '修改', }, addTitle: '新增限制', useWildcard: '是否使用通配符', @@ -98,6 +100,7 @@ const _default: Messages = { paste: '粘貼', save: '保存', delete: '刪除', + modify: '修改', }, addTitle: '新增限製', useWildcard: '是否使用通配符', @@ -134,6 +137,7 @@ const _default: Messages = { paste: 'Paste', save: 'Save', delete: 'Delete', + modify: 'Modify', }, addTitle: 'New', useWildcard: 'Whether to use wildcard', @@ -170,6 +174,7 @@ const _default: Messages = { paste: 'ペースト', save: 'セーブ', delete: '削除', + modify: '変更', }, addTitle: '新增', useWildcard: 'ワイルドカードを使用するかどうか', diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 560fe2d75..bdeb94537 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -13,7 +13,7 @@ import TotalInfoWrapper from "./total-info" import MergeHostWrapper from "./merge-host" import TimeSelectWrapper from "./select/time-select" import TypeSelectWrapper from "./select/type-select" -import timerService, { SortDirect } from "@service/timer-service" +import timerService from "@service/timer-service" import { t } from "@popup/locale" // Import from i18n import { locale } from "@i18n" @@ -136,7 +136,7 @@ class FooterWrapper { date: calculateDateRange(duration, weekStart), mergeHost: this.mergeHostWrapper.mergedHost(), sort: this.typeSelectWrapper.getSelectedType(), - sortOrder: SortDirect.DESC, + sortOrder: 'DESC', chartTitle: t(msg => msg.chart.title[duration]), mergeDate: true, } diff --git a/src/service/timer-service.ts b/src/service/timer-service.ts index 8fe4eab2d..0951c2815 100644 --- a/src/service/timer-service.ts +++ b/src/service/timer-service.ts @@ -26,10 +26,7 @@ const hostAliasDatabase = new HostAliasDatabase(storage) const mergeRuleDatabase = new MergeRuleDatabase(storage) const optionDatabase = new OptionDatabase(storage) -export enum SortDirect { - ASC = 1, - DESC = -1 -} +export type SortDirect = 'ASC' | 'DESC' export type TimerQueryParam = TimerCondition & { /** @@ -145,12 +142,12 @@ class TimerService { const { sort, sortOrder } = param if (!sort) return - const order = sortOrder || SortDirect.ASC + const order = sortOrder || 'ASC' origin.sort((a, b) => { const aa = a[sort] const bb = b[sort] if (aa === bb) return 0 - return order * (aa > bb ? 1 : -1) + return (order === 'ASC' ? 1 : -1) * (aa > bb ? 1 : -1) }) } From 8c05f59f7adcaae9e44181083e6e1354f10dcede Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 10 Jan 2023 03:50:32 +0000 Subject: [PATCH 077/168] Fixbugs in service worker --- src/common/logger.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/common/logger.ts b/src/common/logger.ts index 867494c48..40a7f3cbe 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -8,7 +8,24 @@ const STORAGE_KEY = "_logOpen" const STORAGE_VAL = "1" -let OPEN_LOG = localStorage.getItem(STORAGE_KEY) === STORAGE_VAL +let OPEN_LOG = false + +// localStorage is not undefined in mv3 of service worker +function initOpenLog() { + try { + localStorage.getItem(STORAGE_KEY) === STORAGE_VAL + } catch (ignored) { } +} + +function updateLocalStorage(openState: boolean) { + try { + openState + ? localStorage.setItem(STORAGE_KEY, STORAGE_VAL) + : localStorage.removeItem(STORAGE_KEY) + } catch (ignored) { } +} + +initOpenLog() /** * @since 0.0.4 @@ -22,8 +39,7 @@ export function log(...args: any) { * @since 0.0.4 */ export function openLog(): string { - OPEN_LOG = true - localStorage.setItem(STORAGE_KEY, STORAGE_VAL) + updateLocalStorage(OPEN_LOG = true) return 'Opened the log manually.' } @@ -31,7 +47,6 @@ export function openLog(): string { * @since 0.0.8 */ export function closeLog(): string { - OPEN_LOG = false - localStorage.removeItem(STORAGE_KEY) + updateLocalStorage(OPEN_LOG = false) return 'Closed the log manually.' } \ No newline at end of file From 1e961a3f4ebf51e47bf45b30596e07679b6463c6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 10 Jan 2023 19:23:25 +0800 Subject: [PATCH 078/168] Support to test url for daily limit (#176) --- src/app/components/limit/filter.ts | 11 +++- src/app/components/limit/index.ts | 14 ++-- src/app/components/limit/test.ts | 102 +++++++++++++++++++++++++++++ src/i18n/message/app/limit.ts | 56 ++++++++-------- webpack/webpack.prod.ts | 2 +- 5 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 src/app/components/limit/test.ts diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts index 7b149e841..9bb1e39da 100644 --- a/src/app/components/limit/filter.ts +++ b/src/app/components/limit/filter.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { Plus } from "@element-plus/icons-vue" +import { Operation, Plus } from "@element-plus/icons-vue" import { Ref, h, defineComponent, ref } from "vue" import InputFilterItem from "@app/components/common/input-filter-item" import SwitchFilterItem from "@app/components/common/switch-filter-item" @@ -20,6 +20,7 @@ export type LimitFilterOption = { const urlPlaceholder = t(msg => msg.limit.conditionFilter) const onlyEnabledLabel = t(msg => msg.limit.filterDisabled) const addButtonText = t(msg => msg.limit.button.add) +const testButtonText = t(msg => msg.limit.button.test) const _default = defineComponent({ name: "LimitFilter", @@ -27,7 +28,7 @@ const _default = defineComponent({ url: String, onlyEnabled: Boolean }, - emits: ["create", "change"], + emits: ["create", "change", "test"], setup(props, ctx) { const url: Ref = ref(props.url) const onlyEnabled: Ref = ref(props.onlyEnabled) @@ -52,6 +53,12 @@ const _default = defineComponent({ handleChange() } }), + h(ButtonFilterItem, { + text: testButtonText, + type: 'primary', + icon: Operation, + onClick: () => ctx.emit('test') + }), h(ButtonFilterItem, { text: addButtonText, type: "success", diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index 1e041dd13..6882407df 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -9,7 +9,8 @@ import { defineComponent, h, ref, Ref } from "vue" import ContentContainer from "../common/content-container" import LimitFilter, { LimitFilterOption } from "./filter" import LimitTable from "./table" -import Modify from "./modify" +import LimitModify from "./modify" +import LimitTest from "./test" import limitService from "@service/limit-service" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" @@ -33,6 +34,7 @@ const _default = defineComponent({ urlParam && (url.value = decodeURIComponent(urlParam)) const modify: Ref = ref() + const test: Ref = ref() return () => h(ContentContainer, {}, { filter: () => h(LimitFilter, { @@ -43,7 +45,8 @@ const _default = defineComponent({ onlyEnabled.value = option.onlyEnabled queryData() }, - onCreate: () => modify.value?.create?.() + onCreate: () => modify.value?.create?.(), + onTest: () => test.value?.show?.(), }), content: () => [ h(LimitTable, { @@ -59,10 +62,13 @@ const _default = defineComponent({ modify.value?.modify?.(row) } }), - h(Modify, { + h(LimitModify, { ref: modify, onSave: queryData - }) + }), + h(LimitTest, { + ref: test + }), ] }) } diff --git a/src/app/components/limit/test.ts b/src/app/components/limit/test.ts new file mode 100644 index 000000000..5b5f0295e --- /dev/null +++ b/src/app/components/limit/test.ts @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" +import limitService from "@service/limit-service" +import { ElAlert, ElButton, ElDialog, ElFormItem, ElInput } from "element-plus" +import { defineComponent, Ref, ref, h, ComputedRef, computed } from "vue" + +async function handleTest(url: string): Promise { + const items = await limitService.select({ url, filterDisabled: true }) + return items.map(v => v.cond) +} + +function computeResultTitle(url: string, inputting: boolean, matchedCondition: string[]): string { + if (!url) { + return t(msg => msg.limit.message.inputTestUrl) + } + if (inputting) { + return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.limit.button.testSimple) }) + } + if (!matchedCondition?.length) { + return t(msg => msg.limit.message.noRuleMatched) + } else { + return t(msg => msg.limit.message.rulesMatched) + } +} + +function computeResultDesc(url: string, inputting: boolean, matchedCondition: string[]): string[] { + if (!url || inputting || !matchedCondition?.length) { + return [] + } + return matchedCondition +} + +type _ResultType = 'info' | 'success' | 'warning' + +function computeResultType(url: string, inputting: boolean, matchedCondition: string[]): _ResultType { + if (!url || inputting) { + return 'info' + } + return matchedCondition?.length ? 'success' : 'warning' +} + +const _default = defineComponent({ + name: "LimitTest", + setup(_props, ctx) { + const urlRef: Ref = ref() + const matchedConditionRef: Ref = ref([]) + const visible: Ref = ref(false) + const urlInputtingRef: Ref = ref(true) + const resultTitleRef: ComputedRef = computed(() => computeResultTitle(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultTypeRef: ComputedRef<_ResultType> = computed(() => computeResultType(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultDescRef: ComputedRef = computed(() => computeResultDesc(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + + const changeInput = (newVal: string) => (urlInputtingRef.value = true) && (urlRef.value = newVal?.trim()) + const test = () => { + urlInputtingRef.value = false + handleTest(urlRef.value).then(matched => matchedConditionRef.value = matched) + } + + ctx.expose({ + show() { + urlRef.value = '' + visible.value = true + urlInputtingRef.value = true + matchedConditionRef.value = [] + } + }) + return () => h(ElDialog, { + title: t(msg => msg.limit.button.testSimple), + modelValue: visible.value, + closeOnClickModal: false, + onClose: () => visible.value = false + }, () => [ + h(ElFormItem, { + label: t(msg => msg.limit.testUrlLabel), + labelWidth: 120 + }, () => h(ElInput, { + modelValue: urlRef.value, + clearable: true, + onClear: () => changeInput(''), + onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && test(), + onInput: (newVal: string) => changeInput(newVal) + }, { + append: () => h(ElButton, { + onClick: () => test() + }, () => t(msg => msg.limit.button.testSimple)), + })), + h(ElAlert, { + closable: false, + type: resultTypeRef.value, + title: resultTitleRef.value, + }, () => resultDescRef.value.map(desc => h('li', desc))) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index e9cd01740..50a2fc8e4 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -11,7 +11,6 @@ export type LimitMessage = { addTitle: string useWildcard: string urlPlaceholder: string - item: { condition: string time: string @@ -24,16 +23,12 @@ export type LimitMessage = { button: { add: string test: string + testSimple: string paste: string save: string delete: string modify: string } - timeUnit: { - hour: string - minute: string - second: string - } message: { noUrl: string noTime: string @@ -41,7 +36,12 @@ export type LimitMessage = { deleteConfirm: string deleted: string noPermissionFirefox: string + inputTestUrl: string + clickTestButton: string + noRuleMatched: string + rulesMatched: string } + testUrlLabel: string } const _default: Messages = { @@ -60,6 +60,7 @@ const _default: Messages = { button: { add: '新增', test: '网址测试', + testSimple: '测试', paste: '粘贴', save: '保存', delete: '删除', @@ -67,11 +68,6 @@ const _default: Messages = { }, addTitle: '新增限制', useWildcard: '是否使用通配符', - timeUnit: { - hour: '小时', - minute: '分钟', - second: '秒', - }, message: { saved: '保存成功', noUrl: '未填写限制网址', @@ -79,7 +75,12 @@ const _default: Messages = { deleteConfirm: '是否删除限制:{cond}?', deleted: '删除成功', noPermissionFirefox: '请先在插件管理页[about:addons]开启该插件的粘贴板权限', + inputTestUrl: '请先输入需要测试的网址链接', + clickTestButton: '输入完成后请点击【{buttonText}】按钮', + noRuleMatched: '该网址未命中任何规则', + rulesMatched: '该网址命中以下规则:', }, + testUrlLabel: '测试网址', urlPlaceholder: '请直接粘贴网址 ➡️', }, zh_TW: { @@ -97,6 +98,7 @@ const _default: Messages = { button: { add: '新增', test: '網址測試', + testSimple: '測試', paste: '粘貼', save: '保存', delete: '刪除', @@ -104,11 +106,6 @@ const _default: Messages = { }, addTitle: '新增限製', useWildcard: '是否使用通配符', - timeUnit: { - hour: '小時', - minute: '分鐘', - second: '秒', - }, message: { saved: '保存成功', noUrl: '未填冩限製網址', @@ -116,8 +113,13 @@ const _default: Messages = { deleteConfirm: '是否刪除限製:{cond}?', deleted: '刪除成功', noPermissionFirefox: '請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限', + inputTestUrl: '請先輸入需要測試的網址鏈接', + clickTestButton: '輸入完成後請點擊【{buttonText}】按鈕', + noRuleMatched: '該網址未命中任何規則', + rulesMatched: '該網址命中以下規則:', }, urlPlaceholder: '請直接粘貼網址 ➡️', + testUrlLabel: '測試網址', }, en: { conditionFilter: 'URL', @@ -134,6 +136,7 @@ const _default: Messages = { button: { add: 'New', test: 'Test URL', + testSimple: 'Test', paste: 'Paste', save: 'Save', delete: 'Delete', @@ -141,11 +144,6 @@ const _default: Messages = { }, addTitle: 'New', useWildcard: 'Whether to use wildcard', - timeUnit: { - hour: 'Hours', - minute: 'Minutes', - second: 'Seconds', - }, message: { saved: 'Saved successfully', noUrl: 'Unfilled limited URL', @@ -153,8 +151,13 @@ const _default: Messages = { deleteConfirm: 'Do you want to delete the rule of {cond}?', deleted: 'Deleted successfully', noPermissionFirefox: 'Please enable the clipboard permission of this addon on the management page (about:addons) first', + inputTestUrl: 'Please enter the URL link to be tested first', + clickTestButton: 'After inputting, please click the button ({buttonText})', + noRuleMatched: 'The URL does not hit any rules', + rulesMatched: 'The URL hits the following rules:', }, urlPlaceholder: 'Please paste the URL directly ➡️', + testUrlLabel: 'Test URL', }, ja: { conditionFilter: 'URL', @@ -171,6 +174,7 @@ const _default: Messages = { button: { add: '新增', test: 'テストURL', + testSimple: 'テスト', paste: 'ペースト', save: 'セーブ', delete: '削除', @@ -178,11 +182,6 @@ const _default: Messages = { }, addTitle: '新增', useWildcard: 'ワイルドカードを使用するかどうか', - timeUnit: { - hour: '時間', - minute: '分', - second: '秒', - }, message: { noUrl: '埋められていない制限URL', noTime: '1日の制限時間を記入しない', @@ -190,8 +189,13 @@ const _default: Messages = { deleteConfirm: '{cond} の制限を削除しますか?', deleted: '正常に削除', noPermissionFirefox: '最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください', + inputTestUrl: '最初にテストする URL リンクを入力してください', + clickTestButton: '入力後、ボタン({buttonText})をクリックしてください', + noRuleMatched: 'URL がどのルールとも一致しません', + rulesMatched: 'URL は次のルールに一致します。', }, urlPlaceholder: 'URLを直接貼り付けてください➡️', + testUrlLabel: 'テスト URL', }, } diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index 2aea4c1c4..baa398ba9 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"] +const srcDir = ['public', 'src', "test", 'package.json', 'tsconfig.json', 'webpack', 'global.d.ts', "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') From 9b5694b7a6c7de3f34216a35b02d847f0c171793 Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 11 Jan 2023 10:51:28 +0800 Subject: [PATCH 079/168] Ignore mv3 output directories --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 25488f8eb..ceeb38226 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules dist dist_dev +dist_dev_mv3 dist_dev_safari dist_prod dist_prod_safari @@ -10,6 +11,7 @@ Timer_Safari_DEV Timer firefox_dev +firefox_dev_mv3 market_packages From e7ddf377eab9d272df95d38e80db4ef367bb120c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 12 Jan 2023 00:00:12 +0800 Subject: [PATCH 080/168] Show current users --- script/user-chart/render.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index 34ebe1ca0..3214a63f6 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -136,10 +136,13 @@ function render2Svg(chartData: ChartData): string { width: 960, height: 640 }) + const totalUserCount = Object.values(yAixses) + .map(v => v[v.length - 1] || 0) + .reduce((a, b) => a + b) chart.setOption({ title: { text: 'Total Active User Count', - subtext: `${xAixs[0]} to ${xAixs[xAixs.length - 1]}` + subtext: `${xAixs[0]} to ${xAixs[xAixs.length - 1]} | currently ${totalUserCount} ` }, legend: { data: ALL_BROWSERS }, grid: { From 385a7d3abe26302d6a26a68681a6660bc2f840e4 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 12 Jan 2023 21:46:44 +0800 Subject: [PATCH 081/168] Refactor and clean code --- global.d.ts | 68 +-------- .../components/common/button-filter-item.ts | 5 +- .../common/date-range-filter-item.ts | 4 +- .../components/common/input-filter-item.ts | 4 +- src/app/components/common/number-grow.ts | 1 - src/app/components/common/pagination.ts | 5 +- .../components/common/popup-confirm-button.ts | 5 +- .../components/common/select-filter-item.ts | 15 +- .../components/common/switch-filter-item.ts | 37 ++--- .../dashboard/components/calendar-heat-map.ts | 16 +-- .../dashboard/components/indicator/index.ts | 6 +- .../components/data-manage/clear/clear.d.ts | 7 + .../data-manage/clear/filter/date-filter.ts | 67 +++++---- .../data-manage/clear/filter/delete-button.ts | 28 ++++ .../data-manage/clear/filter/index.ts | 97 +++++++------ .../data-manage/clear/filter/number-filter.ts | 44 ++++-- src/app/components/data-manage/clear/index.ts | 132 +++++++++++++++--- src/app/components/data-manage/index.ts | 50 +++---- src/app/components/data-manage/memory-info.ts | 49 ++++--- src/app/components/data-manage/migration.ts | 89 +++++------- src/app/components/data-manage/style.sass | 43 ++++++ src/app/components/habit/component/filter.ts | 14 +- src/app/components/habit/habit.d.ts | 5 + src/app/components/habit/index.ts | 1 - src/app/components/limit/filter.ts | 15 +- src/app/components/limit/index.ts | 2 +- src/app/components/limit/limit.d.ts | 4 + src/app/components/limit/modify/footer.ts | 7 +- src/app/components/limit/modify/form/index.ts | 4 +- .../components/limit/modify/form/path-edit.ts | 4 +- .../limit/modify/form/time-limit.ts | 4 +- src/app/components/limit/modify/form/url.ts | 48 ++++--- src/app/components/limit/modify/index.ts | 4 +- .../components/limit/table/column/delay.ts | 4 +- .../components/limit/table/column/enabled.ts | 4 +- .../limit/table/column/operation.ts | 5 +- src/app/components/limit/table/index.ts | 13 +- .../components/appearance/dark-mode-input.ts | 4 +- src/app/components/report/file-export.ts | 8 +- .../components/report/filter/download-file.ts | 6 +- src/app/components/report/filter/index.ts | 10 +- .../components/report/filter/remote-client.ts | 4 +- src/app/components/report/index.ts | 26 ++-- src/app/components/report/report.d.ts | 42 ++++++ .../report/table/columns/alias-info.ts | 12 +- .../components/report/table/columns/alias.ts | 4 +- .../table/columns/operation-delete-button.ts | 7 +- .../report/table/columns/operation.ts | 11 +- src/app/components/report/table/index.ts | 17 +-- .../rule-merge/components/add-button.ts | 10 +- .../rule-merge/components/item-input.ts | 9 +- .../components/rule-merge/components/item.ts | 13 +- src/app/components/rule-merge/item-list.ts | 6 +- src/app/components/site-manage/filter.ts | 13 +- src/app/components/site-manage/index.ts | 2 +- .../site-manage/modify/host-form-item.ts | 4 +- .../components/site-manage/modify/index.ts | 16 ++- .../site-manage/modify/name-form-item.ts | 5 +- .../components/site-manage/site-manage.d.ts | 5 + .../site-manage/table/column/operation.ts | 22 ++- src/app/components/site-manage/table/index.ts | 5 +- .../trend/components/chart/index.ts | 2 +- .../trend/components/chart/wrapper.ts | 4 +- src/app/components/trend/components/common.ts | 2 +- src/app/components/trend/components/filter.ts | 22 +-- src/app/components/trend/index.ts | 10 +- src/app/components/trend/trend.d.ts | 17 +++ .../whitelist/components/add-button.ts | 10 +- .../whitelist/components/item-input.ts | 9 +- .../components/whitelist/components/item.ts | 13 +- src/app/components/whitelist/item-list.ts | 10 +- src/guide/layout/menu.ts | 5 +- src/popup/components/chart/click-handler.ts | 2 +- 73 files changed, 767 insertions(+), 510 deletions(-) create mode 100644 src/app/components/data-manage/clear/clear.d.ts create mode 100644 src/app/components/data-manage/clear/filter/delete-button.ts create mode 100644 src/app/components/data-manage/style.sass create mode 100644 src/app/components/habit/habit.d.ts create mode 100644 src/app/components/limit/limit.d.ts create mode 100644 src/app/components/report/report.d.ts create mode 100644 src/app/components/site-manage/site-manage.d.ts create mode 100644 src/app/components/trend/trend.d.ts diff --git a/global.d.ts b/global.d.ts index 2eb2acbb4..88625ada1 100644 --- a/global.d.ts +++ b/global.d.ts @@ -273,20 +273,12 @@ declare namespace timer { namespace limit { /** + * Limit rule in runtime + * * @since 0.8.4 */ - type Item = { - /** - * Condition, can be regular expression with star signs - */ - cond: string + type Item = Rule & { regular: RegExp - /** - * Time limit, seconds - */ - time: number - enabled: boolean - allowDelay: boolean /** * Waste today, milliseconds */ @@ -422,60 +414,6 @@ declare namespace timer { | "second" | "minute" | "hour" - - namespace trend { - type HostInfo = { - host: string - merged: boolean - } - - type FilterOption = { - host: HostInfo, - dateRange: Date[], - timeFormat: TimeFormat - } - - type RenderOption = FilterOption & { - /** - * Whether render firstly - */ - isFirst: boolean - } - } - - namespace report { - /** - * The query param of report page - */ - type QueryParam = { - /** - * Merge host - */ - mh?: string - /** - * Date start - */ - ds?: string - /** - * Date end - */ - de?: string - /** - * Sorted column - */ - sc?: timer.stat.Dimension - } - type FilterOption = { - host: string - dateRange: Date[] - mergeDate: boolean - mergeHost: boolean - /** - * @since 1.1.7 - */ - timeFormat: TimeFormat - } - } } /** diff --git a/src/app/components/common/button-filter-item.ts b/src/app/components/common/button-filter-item.ts index eb501eea0..4d00438ef 100644 --- a/src/app/components/common/button-filter-item.ts +++ b/src/app/components/common/button-filter-item.ts @@ -10,6 +10,7 @@ import ElementIcon from "@src/element-ui/icon" import { ElButton } from "element-plus" import { defineComponent, PropType, h, Ref, computed } from "vue" + const _default = defineComponent({ name: "ButtonFilterItem", props: { @@ -21,7 +22,9 @@ const _default = defineComponent({ default: true } }, - emits: ["click"], + emits: { + click: () => true + }, setup(props, ctx) { const clz: Ref = computed(() => `filter-item${props.right ? " filter-item-right" : ""}`) return () => h(ElButton, { diff --git a/src/app/components/common/date-range-filter-item.ts b/src/app/components/common/date-range-filter-item.ts index 151e0c8b9..1b9b2cc95 100644 --- a/src/app/components/common/date-range-filter-item.ts +++ b/src/app/components/common/date-range-filter-item.ts @@ -23,7 +23,9 @@ const _default = defineComponent({ default: true } }, - emits: ["change"], + emits: { + change: (_value: Date[]) => true + }, setup(props, ctx) { // @ts-ignore const dateRange: Ref = ref(props.defaultRange || [undefined, undefined]) diff --git a/src/app/components/common/input-filter-item.ts b/src/app/components/common/input-filter-item.ts index bbfb46542..738eb6551 100644 --- a/src/app/components/common/input-filter-item.ts +++ b/src/app/components/common/input-filter-item.ts @@ -13,7 +13,9 @@ const _default = defineComponent({ props: { placeholder: String }, - emits: ["search"], + emits: { + search: (_text: string) => true + }, setup(props, ctx) { const modelValue: Ref = ref("") return () => h(ElInput, { diff --git a/src/app/components/common/number-grow.ts b/src/app/components/common/number-grow.ts index d5c0e41c7..2ff384c05 100644 --- a/src/app/components/common/number-grow.ts +++ b/src/app/components/common/number-grow.ts @@ -17,7 +17,6 @@ const _default = defineComponent({ duration: Number, fontSize: Number, }, - emits: ['stop'], setup(props) { const elRef: Ref = ref() const countUp: Ref = ref() diff --git a/src/app/components/common/pagination.ts b/src/app/components/common/pagination.ts index 7a31b277c..45da29fb2 100644 --- a/src/app/components/common/pagination.ts +++ b/src/app/components/common/pagination.ts @@ -15,7 +15,10 @@ const _default = defineComponent({ size: Number, total: Number }, - emits: ["sizeChange", "numChange"], + emits: { + sizeChange: (_val: number) => true, + numChange: (_val: number) => true, + }, setup(props, ctx) { return () => h('div', { class: 'pagination-container' }, h(ElPagination, { diff --git a/src/app/components/common/popup-confirm-button.ts b/src/app/components/common/popup-confirm-button.ts index f11ce34f6..8446637a4 100644 --- a/src/app/components/common/popup-confirm-button.ts +++ b/src/app/components/common/popup-confirm-button.ts @@ -26,7 +26,10 @@ const _default = defineComponent({ default: true } }, - emits: ["confirm", "referenceClick"], + emits: { + confirm: () => true, + referenceClick: () => true, + }, setup(props, ctx) { const display = computed(() => props.visible ? "inline-block" : "none") return () => h(ElPopconfirm, { diff --git a/src/app/components/common/select-filter-item.ts b/src/app/components/common/select-filter-item.ts index cd40fd527..b4c68fc2d 100644 --- a/src/app/components/common/select-filter-item.ts +++ b/src/app/components/common/select-filter-item.ts @@ -1,6 +1,5 @@ import type { PropType, Ref } from "vue" -import type { RouteLocation } from "vue-router" import { watch } from "vue" import { ElOption, ElSelect } from "element-plus" @@ -8,16 +7,6 @@ import { defineComponent, h, ref, nextTick } from "vue" import { useRoute } from "vue-router" import FilterItemHistoryWrapper from "./filter-item-history-wrapper" -const PREFIX = "__filter_select_history_value_" - -function calcHistoryKey(route: RouteLocation, historyName: string): string { - if (!historyName) { - return undefined - } else { - return PREFIX + route.path + '_' + historyName - } -} - const _default = defineComponent({ name: "SelectFilterItem", props: { @@ -31,7 +20,9 @@ const _default = defineComponent({ }, options: Object as PropType> }, - emits: ['select'], + emits: { + select: (_val: string) => true + }, setup(props, ctx) { const modelValue: Ref = ref(props.defaultValue) const historyWrapper = new FilterItemHistoryWrapper(useRoute().path, props.historyName) diff --git a/src/app/components/common/switch-filter-item.ts b/src/app/components/common/switch-filter-item.ts index 97c7344ac..7a3fb573f 100644 --- a/src/app/components/common/switch-filter-item.ts +++ b/src/app/components/common/switch-filter-item.ts @@ -5,43 +5,26 @@ * https://opensource.org/licenses/MIT */ -import { nextTick, Ref } from "vue" -import type { RouteLocation } from "vue-router" +import type { Ref } from "vue" import { ElSwitch } from "element-plus" -import { defineComponent, h, ref } from "vue" +import { defineComponent, h, nextTick, ref } from "vue" import { useRoute } from "vue-router" import FilterItemHistoryWrapper from "./filter-item-history-wrapper" -const PREFIX = "__filter_select_history_value_" - -function calcHistoryKey(route: RouteLocation, historyName: string): string { - if (!historyName) { - return undefined - } else { - return PREFIX + route.path + '_' + historyName - } +type _Props = { + defaultValue?: boolean + historyName?: string + label: string } -const _default = defineComponent({ +const _default = defineComponent<_Props>({ name: "SwitchFilterItem", - props: { - defaultValue: { - type: Boolean, - default: false - }, - /** - * Whether to save the value in the localstorage with {@param historyName} - */ - historyName: { - type: String, - required: false - }, - label: String + emits: { + change: (_val: boolean) => true }, - emits: ["change"], setup(props, ctx) { - const modelValue: Ref = ref(props.defaultValue) + const modelValue: Ref = ref(props.defaultValue || false) const historyWrapper = new FilterItemHistoryWrapper(useRoute().path, props.historyName) // Initiliaze value historyWrapper.ifPresent( diff --git a/src/app/components/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 420364967..1bdc5e277 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -65,9 +65,9 @@ type EcOption = ComposeOption< function formatTooltip(minutes: number, date: string): string { const hour = Math.floor(minutes / 60) const minute = minutes % 60 - const year = date.substr(0, 4) - const month = date.substr(4, 2) - const day = date.substr(6, 2) + const year = date.substring(0, 4) + const month = date.substring(4, 6) + const day = date.substring(6, 8) const placeholders = { hour, minute, year, month, day } @@ -90,7 +90,7 @@ function getXAxisLabelMap(data: _Value[]): { [x: string]: string } { const allMonthLabel = t(msg => msg.calendar.months).split('|') const result = {} // {[ x:string ]: Set } - const xAndMonthMap = groupBy(data, e => e[0], grouped => new Set(grouped.map(a => a[3].substr(4, 2)))) + const xAndMonthMap = groupBy(data, e => e[0], grouped => new Set(grouped.map(a => a[3].substring(4, 6)))) let lastMonth = undefined Object.entries(xAndMonthMap).forEach(([x, monthSet]) => { if (monthSet.size != 1) { @@ -206,11 +206,11 @@ function handleClick(value: _Value): void { return } - const currentYear = parseInt(currentDate.substr(0, 4)) - const currentMonth = parseInt(currentDate.substr(4, 2)) - 1 - const currentDay = parseInt(currentDate.substr(6, 2)) + const currentYear = parseInt(currentDate.substring(0, 4)) + const currentMonth = parseInt(currentDate.substring(4, 6)) - 1 + const currentDay = parseInt(currentDate.substring(6, 8)) const currentTs = (new Date(currentYear, currentMonth, currentDay).getTime() + 1000).toString() - const query: timer.app.report.QueryParam = { ds: currentTs, de: currentTs } + const query: ReportQueryParam = { ds: currentTs, de: currentTs } const url = getAppPageUrl(false, REPORT_ROUTE, query) chrome.tabs.create({ url }) diff --git a/src/app/components/dashboard/components/indicator/index.ts b/src/app/components/dashboard/components/indicator/index.ts index 0ba9e2782..fc031f92a 100644 --- a/src/app/components/dashboard/components/indicator/index.ts +++ b/src/app/components/dashboard/components/indicator/index.ts @@ -80,9 +80,9 @@ async function query(): Promise<_Value> { // 2. if not exist, calculate from all data items const firstDate = allData.map(a => a.date).filter(d => d?.length === 8).sort()[0] if (firstDate) { - const year = parseInt(firstDate.substr(0, 4)) - const month = parseInt(firstDate.substr(4, 2)) - 1 - const date = parseInt(firstDate.substr(6, 2)) + const year = parseInt(firstDate.substring(0, 4)) + const month = parseInt(firstDate.substring(4, 6)) - 1 + const date = parseInt(firstDate.substring(6, 8)) installTime = new Date(year, month, date) } diff --git a/src/app/components/data-manage/clear/clear.d.ts b/src/app/components/data-manage/clear/clear.d.ts new file mode 100644 index 000000000..89d99ac73 --- /dev/null +++ b/src/app/components/data-manage/clear/clear.d.ts @@ -0,0 +1,7 @@ +declare type DataManageClearFilterOption = { + dateRange: Date[], + focusStart: string, + focusEnd: string, + timeStart: string, + timeEnd: string, +} \ No newline at end of file diff --git a/src/app/components/data-manage/clear/filter/date-filter.ts b/src/app/components/data-manage/clear/filter/date-filter.ts index 3b0c16946..eaea46cb8 100644 --- a/src/app/components/data-manage/clear/filter/date-filter.ts +++ b/src/app/components/data-manage/clear/filter/date-filter.ts @@ -6,16 +6,12 @@ */ import { ElDatePicker } from "element-plus" -import { Ref, h } from "vue" +import { Ref, h, defineComponent, PropType } from "vue" import { formatTime, MILL_PER_DAY } from "@util/time" import { t, tN } from "@app/locale" import { DataManageMessage } from "@i18n/message/app/data-manage" import { stepNoClz } from "./constants" -export type DateFilterProps = { - dateRangeRef: Ref -} - const yesterday = new Date().getTime() - MILL_PER_DAY const daysBefore = (days: number) => new Date().getTime() - days * MILL_PER_DAY @@ -36,31 +32,40 @@ const pickerShortcuts = [ datePickerShortcut('till30DaysAgo', 30) ] +const dateFormat = t(msg => msg.calendar.dateFormat, { y: 'YYYY', m: 'MM', d: 'DD' }) +// The birthday of browser +const startPlaceholder = t(msg => msg.calendar.dateFormat, { y: '1994', m: '12', d: '15' }) +const endPlaceholder = formatTime(yesterday, t(msg => msg.calendar.dateFormat)) -function picker({ dateRangeRef }: DateFilterProps) { - const dateFormat = t(msg => msg.calendar.dateFormat, { y: 'YYYY', m: 'MM', d: 'DD' }) - // The birthday of browser - const startPlaceholder = t(msg => msg.calendar.dateFormat, { y: '1994', m: '12', d: '15' }) - const endPlaceholder = formatTime(yesterday, t(msg => msg.calendar.dateFormat)) - // @ts-ignore - return h(ElDatePicker, { - modelValue: dateRangeRef.value, - "onUpdate:modelValue": (date: Array) => dateRangeRef.value = date, - size: 'small', - style: 'width: 250px;', - startPlaceholder, - format: dateFormat, - endPlaceholder, - type: 'daterange', - disabledDate(date: Date) { return date.getTime() > yesterday }, - shortcuts: pickerShortcuts, - rangeSeparator: '-' - }) -} - -const dateFilter = (props: DateFilterProps) => h('p', [ - h('a', { class: stepNoClz }, '1.'), - tN(msg => msg.dataManage.filterDate, { picker: picker(props) }) -]) +const _default = defineComponent({ + name: "DateFilter", + emits: { + change: (_date: Date[]) => true + }, + props: { + dateRange: Array as PropType + }, + setup(props, ctx) { + return () => h('p', [ + h('a', { class: stepNoClz }, '1.'), + tN(msg => msg.dataManage.filterDate, { + // @ts-ignore + picker: h(ElDatePicker, { + modelValue: props.dateRange, + "onUpdate:modelValue": (date: Array) => ctx.emit('change', date), + size: 'small', + style: 'width: 250px;', + startPlaceholder, + format: dateFormat, + endPlaceholder, + type: 'daterange', + disabledDate(date: Date) { return date.getTime() > yesterday }, + shortcuts: pickerShortcuts, + rangeSeparator: '-' + }) + }) + ]) + } +}) -export default dateFilter +export default _default diff --git a/src/app/components/data-manage/clear/filter/delete-button.ts b/src/app/components/data-manage/clear/filter/delete-button.ts new file mode 100644 index 000000000..1f931b3e1 --- /dev/null +++ b/src/app/components/data-manage/clear/filter/delete-button.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElButton } from "element-plus" +import { h, defineComponent } from "vue" +import { t } from "@src/app/locale" +import { Delete } from "@element-plus/icons-vue" + +const _default = defineComponent({ + name: "DeleteButton", + emits: { + click: () => true + }, + setup(_, ctx) { + return () => h('div', { class: 'footer-container filter-container' }, h(ElButton, { + icon: Delete, + type: 'danger', + size: 'small', + onClick: () => ctx.emit('click') + }, () => t(msg => msg.item.operation.delete))) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/clear/filter/index.ts b/src/app/components/data-manage/clear/filter/index.ts index 0876dea53..8218d6988 100644 --- a/src/app/components/data-manage/clear/filter/index.ts +++ b/src/app/components/data-manage/clear/filter/index.ts @@ -5,63 +5,60 @@ * https://opensource.org/licenses/MIT */ -import type { Ref, SetupContext } from "vue" +import type { Ref } from "vue" -import { Delete } from "@element-plus/icons-vue" import { defineComponent, h, ref } from "vue" -import TimerDatabase from "@db/timer-database" import { t } from "@app/locale" -import dateFilter from "./date-filter" -import numberFilter from "./number-filter" -import operationButton, { BaseFilterProps } from "./operation-button" +import DateFilter from "./date-filter" +import NumberFilter from "./number-filter" +import DeleteButton from "./delete-button" -const timerDatabase = new TimerDatabase(chrome.storage.local) - -const dateRangeRef: Ref> = ref([]) -const focusStartRef: Ref = ref('0') -const focusEndRef: Ref = ref('2') -const timeStartRef: Ref = ref('0') -const timeEndRef: Ref = ref('') - -const title = h('h3', t(msg => msg.dataManage.filterItems)) - -const filterRefs: BaseFilterProps = { - dateRangeRef, - focusStartRef, focusEndRef, - timeStartRef, timeEndRef, -} - -const deleteButton = (onDateChanged: () => void) => operationButton({ - ...filterRefs, - onDateChanged, - - confirm: { - message: 'deleteConfirm', - operation: result => timerDatabase.delete(result), - resultMessage: 'deleteSuccess' +const _default = defineComponent({ + emits: { + delete: () => true }, + setup(_, ctx) { + const dateRangeRef: Ref> = ref([]) + const focusStartRef: Ref = ref('0') + const focusEndRef: Ref = ref('2') + const timeStartRef: Ref = ref('0') + const timeEndRef: Ref = ref('') + const computeFilterOption = () => ({ + dateRange: dateRangeRef.value, + focusStart: focusStartRef.value, + focusEnd: focusEndRef.value, + timeStart: timeStartRef.value, + timeEnd: timeEndRef.value, + } as DataManageClearFilterOption) - button: { - message: 'delete', - icon: Delete, - type: 'danger' - } -}) - -const _default = defineComponent((_props, ctx: SetupContext) => { - const onDateChanged = ctx.attrs.onDateChanged as () => void + ctx.expose({ + getFilterOption: computeFilterOption + }) - const footer = () => h('div', { class: 'footer-container filter-container' }, deleteButton(onDateChanged)) - - return () => h('div', { class: 'clear-panel' }, - [ - title, - dateFilter({ dateRangeRef }), - numberFilter('filterFocus', focusStartRef, focusEndRef, 2), - numberFilter('filterTime', timeStartRef, timeEndRef, 3), - footer() - ] - ) + return () => h('div', { class: 'clear-panel' }, [ + h('h3', t(msg => msg.dataManage.filterItems)), + h(DateFilter, { dateRange: dateRangeRef.value, onChange: (newVal: Date[]) => dateRangeRef.value = newVal }), + h(NumberFilter, { + translateKey: 'filterFocus', + start: focusStartRef.value, + end: focusEndRef.value, + lineNo: 2, + onStartChange: v => focusStartRef.value = v, + onEndChange: v => focusEndRef.value = v, + }), + h(NumberFilter, { + translateKey: 'filterTime', + start: timeStartRef.value, + end: timeEndRef.value, + lineNo: 3, + onStartChange: v => timeStartRef.value = v, + onEndChange: v => timeEndRef.value = v, + }), + h(DeleteButton, { + onClick: () => ctx.emit('delete') + }), + ]) + } }) export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/clear/filter/number-filter.ts b/src/app/components/data-manage/clear/filter/number-filter.ts index 6d8be5f98..236b22549 100644 --- a/src/app/components/data-manage/clear/filter/number-filter.ts +++ b/src/app/components/data-manage/clear/filter/number-filter.ts @@ -6,29 +6,45 @@ */ import { ElInput } from "element-plus" -import { Ref, h } from "vue" +import { h, defineComponent, PropType } from "vue" import { t, tN } from "@app/locale" import { DataManageMessage } from "@i18n/message/app/data-manage" import { stepNoClz } from "./constants" -const elInput = (valRef: Ref, placeholder: string, min?: Ref) => +const elInput = (val: string, setter: (val: string) => void, placeholder: string, min?: string) => h(ElInput, { class: 'filter-input', placeholder: placeholder, - min: min !== undefined ? min.value || '0' : undefined, + min: min !== undefined ? min || '0' : undefined, clearable: true, size: 'small', - modelValue: valRef.value, - onInput: (val: string) => valRef.value = val.trim(), - onClear: () => valRef.value = '' + modelValue: val, + onInput: (val: string) => setter?.(val.trim()), + onClear: () => setter?.('') }) -const numberFilter = (translateKey: keyof DataManageMessage, startRef: Ref, endRef: Ref, lineNo: number) => h('p', [ - h('a', { class: stepNoClz }, `${lineNo}.`), - tN(msg => msg.dataManage[translateKey], { - start: elInput(startRef, '0'), - end: elInput(endRef, t(msg => msg.dataManage.unlimited), startRef) - }) -]) -export default numberFilter \ No newline at end of file +const _default = defineComponent({ + name: "NumberFilter", + props: { + translateKey: String as PropType, + start: String, + end: String, + lineNo: Number, + }, + emits: { + startChange: (_val: string) => true, + endChange: (_val: string) => true, + }, + setup(props, ctx) { + return () => h('p', [ + h('a', { class: stepNoClz }, `${props.lineNo}.`), + tN(msg => msg.dataManage[props.translateKey], { + start: elInput(props.start, v => ctx.emit('startChange', v), '0'), + end: elInput(props.end, v => ctx.emit('endChange', v), t(msg => msg.dataManage.unlimited), props.start) + }) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/clear/index.ts b/src/app/components/data-manage/clear/index.ts index 1faacc054..ef188ca28 100644 --- a/src/app/components/data-manage/clear/index.ts +++ b/src/app/components/data-manage/clear/index.ts @@ -5,26 +5,124 @@ * https://opensource.org/licenses/MIT */ -import { ElAlert, ElCard } from "element-plus" -import { h } from "vue" +import { ElAlert, ElCard, ElMessage, ElMessageBox } from "element-plus" +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 { MILL_PER_DAY } from "@util/time" -type _Props = { - queryData: () => Promise | void +type _Emits = { + dataDelete: () => true } -const clearAlert = () => h(ElAlert, - { - ...alertProps, - title: t(msg => msg.dataManage.operationAlert) - }) -const clearFilter = (queryData: () => Promise | void) => h(Filter, { onDateChanged: queryData }) -const clearPanel = (props: _Props) => h(ElCard, { - class: 'clear-container' -}, - () => [clearAlert(), clearFilter(props.queryData)] -) - -export default clearPanel \ No newline at end of file +const timerDatabase = new TimerDatabase(chrome.storage.local) + +const operationCancelMsg = t(msg => msg.dataManage.operationCancel) +const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) + +async function handleClick(filterRef: Ref, ctx: SetupContext<_Emits>) { + const filterOption: DataManageClearFilterOption = filterRef?.value?.getFilterOption() + const result: timer.stat.Row[] = await generateParamAndSelect(filterOption) + + const count = result.length + const confirmMsg = t(msg => msg.dataManage.deleteConfirm, { count }) + ElMessageBox.confirm(confirmMsg, { + cancelButtonText: operationCancelMsg, + confirmButtonText: operationConfirmMsg + }).then(async () => { + await timerDatabase.delete(result) + ElMessage(t(msg => msg.dataManage.deleteSuccess)) + ctx.emit('dataDelete') + }).catch(() => { }) +} + +function generateParamAndSelect(props: DataManageClearFilterOption): Promise | undefined { + const condition = checkParam(props) + if (!condition) { + ElMessage({ message: t(msg => msg.dataManage.paramError), type: 'warning' }) + return + } + + const { dateRange } = props + let [dateStart, dateEnd] = dateRange || [] + if (dateEnd == null) { + // default end time is the yesterday + dateEnd = new Date(new Date().getTime() - MILL_PER_DAY) + } + condition.date = [dateStart, dateEnd] + + return timerDatabase.select(condition) +} + +/** + * Assert query param with numeric range + * + * @param range numeric range, 2-length array + * @param mustInteger must be integer? + * @returns true when has error, or false + */ +function assertQueryParam(range: number[], mustInteger?: boolean): boolean { + const reg = mustInteger ? /^[0-9]+$/ : /^[0-9]+.?[0-9]*$/ + const start = range[0] + const end = range[1] + const noStart = start !== undefined && start !== null + const noEnd = end !== undefined && end !== null + return (noStart && !reg.test(start.toString())) + || (noEnd && !reg.test(end.toString())) + || (noStart && noEnd && start > end) +} + +const str2Num = (str: string, defaultVal?: number) => (str && str !== '') ? parseInt(str) : defaultVal +const seconds2Milliseconds = (a: number) => a * 1000 + +function checkParam(filterOption: DataManageClearFilterOption): TimerCondition | undefined { + const { focusStart, focusEnd, timeStart, timeEnd } = filterOption + let hasError = false + const focusRange = str2Range([focusStart, focusEnd], seconds2Milliseconds) + hasError = hasError || assertQueryParam(focusRange) + const timeRange = str2Range([timeStart, timeEnd]) + hasError = hasError || assertQueryParam(timeRange, true) + if (hasError) { + return undefined + } + const condition: TimerCondition = {} + condition.focusRange = focusRange + condition.timeRange = timeRange + return condition +} + +function str2Range(startAndEnd: [string, string], numAmplifier?: (origin: number) => number): [number, number] { + const startStr = startAndEnd[0] + const endStr = startAndEnd[1] + let start = str2Num(startStr, 0) + numAmplifier && (start = numAmplifier(start)) + let end = str2Num(endStr) + end && numAmplifier && (end = numAmplifier(end)) + return [start, end] +} + + +const _default = defineComponent({ + emits: { + dataDelete: () => true + }, + setup(_, ctx) { + const filterRef: Ref = ref() + return () => h(ElCard, { + class: 'clear-container' + }, () => [ + h(ElAlert, { + ...alertProps, + title: t(msg => msg.dataManage.operationAlert) + }), + h(Filter, { + ref: filterRef, + onDelete: () => handleClick(filterRef, ctx), + }), + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/index.ts b/src/app/components/data-manage/index.ts index d4f6794b5..2b87a5270 100644 --- a/src/app/components/data-manage/index.ts +++ b/src/app/components/data-manage/index.ts @@ -6,36 +6,26 @@ */ import { ElRow, ElCol } from "element-plus" -import { defineComponent, h, ref, Ref } from "vue" -import { getUsedStorage } from "@db/memory-detector" -import './style' +import { defineComponent, h, Ref, ref } from "vue" import ContentContainer from "../common/content-container" -import migration from "./migration" -import memoryInfo from "./memory-info" -import clearPanel from "./clear" - -// Total memory with byte -const usedRef: Ref = ref(0) -const totalRef: Ref = ref(1) // As the denominator of percentage, cannot be 0, so be 1 - -const queryData = async () => { - const { used, total } = await getUsedStorage() - usedRef.value = used || 0 - totalRef.value = total -} - -queryData() - -const firstRow = () => h(ElRow, { gutter: 20 }, - () => [ - h(ElCol, { span: 8 }, () => memoryInfo({ usedRef, totalRef })), - h(ElCol, { span: 11 }, () => clearPanel({ queryData })), - h(ElCol, { span: 5 }, () => migration({ queryData })) - ] -) +import Migration from "./migration" +import MemeryInfo from "./memory-info" +import ClearPanel from "./clear" +import './style' -export default defineComponent(() => { - return () => h(ContentContainer, { - class: 'data-manage-container' - }, () => firstRow()) +export default defineComponent({ + name: "DataManage", + setup() { + const memeryInfoRef: Ref = ref() + const queryData = () => memeryInfoRef?.value?.queryData() + return () => h(ContentContainer, { + class: 'data-manage-container' + }, () => h(ElRow, { gutter: 20 }, + () => [ + h(ElCol, { span: 8 }, () => h(MemeryInfo, { ref: memeryInfoRef })), + h(ElCol, { span: 11 }, () => h(ClearPanel, { onDataDelete: queryData })), + h(ElCol, { span: 5 }, () => h(Migration, { onImport: queryData })), + ] + )) + } }) \ No newline at end of file diff --git a/src/app/components/data-manage/memory-info.ts b/src/app/components/data-manage/memory-info.ts index 9568ba317..f91b6cf7b 100644 --- a/src/app/components/data-manage/memory-info.ts +++ b/src/app/components/data-manage/memory-info.ts @@ -6,14 +6,10 @@ */ import { ElAlert, ElCard, ElProgress } from "element-plus" -import { h, Ref } from "vue" +import { computed, ComputedRef, defineComponent, h, ref, Ref } from "vue" import { t } from "@app/locale" import { alertProps } from "./common" - -type _Props = { - usedRef: Ref - totalRef: Ref -} +import { getUsedStorage } from "@db/memory-detector" const memoryAlert = (totalMb: number) => { const title = totalMb @@ -43,13 +39,8 @@ const usedAlert = (usedMb: number, typeColor: string) => h('div', { style: usedA ) const byte2Mb = (size: number) => Math.round((size || 0) / 1024.0 / 1024.0 * 1000) / 1000 -const memoryInfo = (props: _Props) => { - const { usedRef, totalRef } = props - const used = usedRef.value - const total = totalRef.value - const usedMb = byte2Mb(used) - const totalMb = byte2Mb(total) - const percentage: number = total ? Math.round(used * 10000.0 / total) / 100 : 0 + +function computeColor(percentage: number, total: number): string { // Danger color let typeColor = '#F56C6C' // Primary color @@ -58,9 +49,33 @@ const memoryInfo = (props: _Props) => { else if (percentage < 75) typeColor = '#E6A23C' // Specially, show warning color if not detect the max memory if (!total) typeColor = '#E6A23C' - return h(ElCard, {}, - () => [memoryAlert(totalMb), memoryProgress(percentage, typeColor), usedAlert(usedMb, typeColor)] - ) + return typeColor } -export default memoryInfo \ No newline at end of file +const _default = defineComponent({ + name: "MemoryInfo", + setup(_, ctx) { + // Total memory with byte + const usedRef: Ref = ref(0) + // As the denominator of percentage, cannot be 0, so be 1 + const totalRef: Ref = ref(1) + const queryData = async () => { + const { used, total } = await getUsedStorage() + usedRef.value = used || 0 + totalRef.value = total + } + queryData() + ctx.expose({ queryData }) + + const usedMbRef: ComputedRef = computed(() => byte2Mb(usedRef.value)) + const totalMbRef: ComputedRef = computed(() => byte2Mb(totalRef.value)) + const percentageRef: ComputedRef = computed(() => totalRef.value ? Math.round(usedRef.value * 10000.0 / totalRef.value) / 100 : 0) + const colorRef: ComputedRef = computed(() => computeColor(percentageRef.value, totalRef.value)) + + return () => h(ElCard, {}, + () => [memoryAlert(totalMbRef.value), memoryProgress(percentageRef.value, colorRef.value), usedAlert(usedMbRef.value, colorRef.value)] + ) + }, +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/migration.ts b/src/app/components/data-manage/migration.ts index 17200937a..06198e84c 100644 --- a/src/app/components/data-manage/migration.ts +++ b/src/app/components/data-manage/migration.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import type { Ref } from "vue" +import { defineComponent, Ref } from "vue" import { h, ref } from "vue" import { ElAlert, ElButton, ElCard, ElLoading, ElMain, ElMessage } from "element-plus" @@ -16,21 +16,16 @@ import { formatTime } from "@util/time" import Immigration from "@service/components/immigration" import { Download, Upload } from "@element-plus/icons-vue" -type _Props = { - queryData: () => void | Promise -} - const immigration: Immigration = new Immigration() -const handleExport = async () => { +async function handleExport() { const data = await immigration.getExportingData() const timestamp = formatTime(new Date(), '{y}{m}{d}_{h}{i}{s}') exportJson(data, `timer_backup_${timestamp}`) } -const fileInputRef: Ref = ref() -const handleFileSelected = async (queryData: () => void) => { - const files: FileList | null = fileInputRef.value.files +async function handleFileSelected(fileInputRef: Ref, callback: () => void) { + const files: FileList | null = fileInputRef?.value?.files if (!files || !files.length) { return } @@ -43,52 +38,44 @@ const handleFileSelected = async (queryData: () => void) => { } await immigration.importData(data) loading.close() - queryData() + callback?.() ElMessage.success(t(msg => msg.dataManage.migrated)) } -const alert = () => h(ElAlert, alertProps, () => t(msg => msg.dataManage.migrationAlert)) - const exportButtonText = t(msg => msg.item.operation.exportWholeData) -const exportButton = () => h(ElButton, - { - size: 'large', - type: 'success', - icon: Download, - onClick: handleExport - }, - () => exportButtonText -) -const fileInputProps = { - ref: fileInputRef, - type: 'file', - accept: '.json', - style: { display: 'none' } -} -const fileInput = (queryData: any) => h('input', { - ...fileInputProps, - onChange: () => handleFileSelected(queryData) -}) -const importButtonText = (queryData: any) => [t(msg => msg.item.operation.importWholeData), fileInput(queryData)] -const importButton = (queryData: any) => h(ElButton, - { - size: 'large', - type: 'primary', - icon: Upload, - onClick: () => fileInputRef.value.click() +const _default = defineComponent({ + name: "Migration", + emits: { + import: () => true }, - () => importButtonText(queryData) -) - -const buttonContainer = (queryData: () => void | Promise) => h(ElMain, {}, - () => [alert(), exportButton(), importButton(queryData)] -) + setup(_, ctx) { + const fileInputRef: Ref = ref() + return () => h(ElCard, { class: 'migration-container' }, () => h(ElMain, {}, () => [ + h(ElAlert, alertProps, () => t(msg => msg.dataManage.migrationAlert)), + h(ElButton, { + size: 'large', + type: 'success', + icon: Download, + onClick: handleExport + }, () => exportButtonText), + h(ElButton, { + size: 'large', + type: 'primary', + icon: Upload, + onClick: () => fileInputRef.value.click() + }, () => [ + t(msg => msg.item.operation.importWholeData), + h('input', { + ref: fileInputRef, + type: 'file', + accept: '.json', + style: { display: 'none' }, + onChange: () => handleFileSelected(fileInputRef, () => ctx.emit('import')) + }) + ]) + ])) + } +}) -export default (props: _Props) => { - return h(ElCard, { - class: 'migration-container' - }, - () => buttonContainer(props.queryData) - ) -} \ No newline at end of file +export default _default \ No newline at end of file diff --git a/src/app/components/data-manage/style.sass b/src/app/components/data-manage/style.sass new file mode 100644 index 000000000..c7e3a0325 --- /dev/null +++ b/src/app/components/data-manage/style.sass @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +.data-manage-container + .el-card__body + height: 450px + text-align: center + .clear-container + .clear-panel + text-align: left + padding-left: 30px + padding-top: 20px + .filter-input + width: 60px + .el-input__wrapper + height: 22px + + .el-input__suffix + right: 0 !important + + .step-no + margin-right: 10px + .filter-container + padding-top: 40px + + .migration-container + .el-main + height: 100% + flex-direction: column + display: flex + justify-content: space-between + .el-button + width: 100% + height: 30% + margin-left: 0px + .el-progress-circle + width: 250px !important + height: 250px !important + margin: auto diff --git a/src/app/components/habit/component/filter.ts b/src/app/components/habit/component/filter.ts index ceb4c11f2..c55df34a1 100644 --- a/src/app/components/habit/component/filter.ts +++ b/src/app/components/habit/component/filter.ts @@ -16,12 +16,6 @@ import { ElementDatePickerShortcut } from "@src/element-ui/date" import DateRangeFilterItem from "@app/components/common/date-range-filter-item" import SelectFilterItem from "@app/components/common/select-filter-item" -export type HabitFilterOption = { - periodSize: number - dateRange: Date[] - averageByDate: boolean -} - type ShortCutProp = [label: keyof HabitMessage['dateRange'], dayAgo: number] const shortcutProps: ShortCutProp[] = [ @@ -60,6 +54,10 @@ function allOptions(): Record { return allOptions } +const emits = { + change: (_option: HabitFilterOption) => true +} + const _default = defineComponent({ name: "HabitFilter", props: { @@ -67,7 +65,7 @@ const _default = defineComponent({ dateRange: Array as PropType, averageByDate: Boolean }, - emits: ["change"], + emits, setup(props, ctx) { const periodSize: Ref = ref(props.periodSize || 1) // @ts-ignore @@ -78,7 +76,7 @@ const _default = defineComponent({ periodSize: periodSize.value, dateRange: dateRange.value, averageByDate: averageByDate.value - } as HabitFilterOption) + }) } return () => [ // Size select diff --git a/src/app/components/habit/habit.d.ts b/src/app/components/habit/habit.d.ts new file mode 100644 index 000000000..e933606cd --- /dev/null +++ b/src/app/components/habit/habit.d.ts @@ -0,0 +1,5 @@ +declare type HabitFilterOption = { + periodSize: number + dateRange: Date[] + averageByDate: boolean +} \ No newline at end of file diff --git a/src/app/components/habit/index.ts b/src/app/components/habit/index.ts index 8139546ed..c0c98e26c 100644 --- a/src/app/components/habit/index.ts +++ b/src/app/components/habit/index.ts @@ -6,7 +6,6 @@ */ import type { Ref } from "vue" -import type { HabitFilterOption } from "./component/filter" import { defineComponent, h, ref, onMounted } from "vue" import periodService from "@service/period-service" diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts index 9bb1e39da..dd8ef2385 100644 --- a/src/app/components/limit/filter.ts +++ b/src/app/components/limit/filter.ts @@ -12,30 +12,31 @@ import SwitchFilterItem from "@app/components/common/switch-filter-item" import ButtonFilterItem from "@app/components/common/button-filter-item" import { t } from "@app/locale" -export type LimitFilterOption = { - url: string - onlyEnabled: boolean -} - const urlPlaceholder = t(msg => msg.limit.conditionFilter) const onlyEnabledLabel = t(msg => msg.limit.filterDisabled) const addButtonText = t(msg => msg.limit.button.add) const testButtonText = t(msg => msg.limit.button.test) +const emits = { + create: () => true, + change: (_option: LimitFilterOption) => true, + test: () => true, +} + const _default = defineComponent({ name: "LimitFilter", props: { url: String, onlyEnabled: Boolean }, - emits: ["create", "change", "test"], + emits, setup(props, ctx) { const url: Ref = ref(props.url) const onlyEnabled: Ref = ref(props.onlyEnabled) const handleChange = () => ctx.emit("change", { url: url.value, onlyEnabled: onlyEnabled.value - } as LimitFilterOption) + }) return () => [ h(InputFilterItem, { placeholder: urlPlaceholder, diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index 6882407df..6813a0762 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -7,7 +7,7 @@ import { defineComponent, h, ref, Ref } from "vue" import ContentContainer from "../common/content-container" -import LimitFilter, { LimitFilterOption } from "./filter" +import LimitFilter from "./filter" import LimitTable from "./table" import LimitModify from "./modify" import LimitTest from "./test" diff --git a/src/app/components/limit/limit.d.ts b/src/app/components/limit/limit.d.ts new file mode 100644 index 000000000..895a22bb7 --- /dev/null +++ b/src/app/components/limit/limit.d.ts @@ -0,0 +1,4 @@ +declare type LimitFilterOption = { + url: string + onlyEnabled: boolean +} diff --git a/src/app/components/limit/modify/footer.ts b/src/app/components/limit/modify/footer.ts index e3a5d9387..8e1f3a67e 100644 --- a/src/app/components/limit/modify/footer.ts +++ b/src/app/components/limit/modify/footer.ts @@ -13,10 +13,11 @@ import { t } from "@app/locale" const buttonText = t(msg => msg.limit.button.save) const _default = defineComponent({ name: "SaveButton", - emits: ["save"], + emits: { + save: () => true + }, setup(_, ctx) { - return () => h('span', - {}, + return () => h('span', {}, h(ElButton, { onClick: () => ctx.emit("save"), type: 'primary', diff --git a/src/app/components/limit/modify/form/index.ts b/src/app/components/limit/modify/form/index.ts index 2315bf3b4..0296d1a04 100644 --- a/src/app/components/limit/modify/form/index.ts +++ b/src/app/components/limit/modify/form/index.ts @@ -93,9 +93,7 @@ const _default = defineComponent({ ref: pathEditRef, disabled: editMode.value === 'modify', url: urlInfo.url, - onUrlChange: (newVal: string) => { - urlInfo.url = newVal - } + onUrlChange: (newVal: string) => urlInfo.url = newVal }) ) return items diff --git a/src/app/components/limit/modify/form/path-edit.ts b/src/app/components/limit/modify/form/path-edit.ts index 50711af20..8ca5cdb51 100644 --- a/src/app/components/limit/modify/form/path-edit.ts +++ b/src/app/components/limit/modify/form/path-edit.ts @@ -73,7 +73,9 @@ const _default = defineComponent({ defaultValue: '' }, }, - emits: ['urlChange'], + emits: { + urlChange: (_url: string) => true + }, setup(props, ctx) { const url = props.url const items: Ref = ref(url2PathItems(url)) diff --git a/src/app/components/limit/modify/form/time-limit.ts b/src/app/components/limit/modify/form/time-limit.ts index 88bc67d2f..14a5042a9 100644 --- a/src/app/components/limit/modify/form/time-limit.ts +++ b/src/app/components/limit/modify/form/time-limit.ts @@ -61,7 +61,9 @@ const _default = defineComponent({ type: Number } }, - emits: ['change'], + emits: { + change: (_val: number) => true + }, setup(props, ctx) { const [hour, minute, second] = computeSecond2LimitInfo(props.modelValue) const hourRef: Ref = ref(hour) diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index 4f33a92de..b595c63d6 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { defineComponent, h, PropType, ref, Ref, watch } from "vue" +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" @@ -35,6 +35,26 @@ function cleanUrl(url: string): string { const PERMISSION = 'clipboardRead' const FIREFOX_NO_PERMISSION_MSG = t(msg => msg.limit.message.noPermissionFirefox) +type _Slot = () => VNode +type _Slots = { prefix: _Slot, append?: _Slot } + +function slots(protocolRef: Ref, urlRef: Ref, disabled: boolean): _Slots { + const slots: _Slots = { + prefix: () => h(ElSelect, { + modelValue: protocolRef.value, + onChange: (val: string) => protocolRef.value = val as Protocol, + disabled: disabled, + }, protocolOptions), + } + !disabled && (slots.append = () => h(ElButton, { + onClick: () => handlePaste( + url => urlRef.value = url, + prot => protocolRef.value = prot + ) + }, () => pasteButtonText)) + return slots +} + async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler: (newProtocol: Protocol) => void) { let granted = await checkPermission(PERMISSION) @@ -62,12 +82,14 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler } const pasteButtonText = t(msg => msg.limit.button.paste) - const placeholder = t(msg => msg.limit.urlPlaceholder) const _default = defineComponent({ name: 'LimitUrlFormItem', - emits: ['urlChange', 'protocolChange'], + emits: { + urlChange: (_val: string) => true, + protocolChange: (_val: Protocol) => true, + }, props: { url: String, protocol: String as PropType, @@ -88,23 +110,7 @@ const _default = defineComponent({ return () => h(ElFormItem, { label: t(msg => msg.limit.item.condition) }, () => { - const slots: any = { - prefix: () => h(ElSelect, { - modelValue: protocolRef.value, - onChange: (val: string) => protocolRef.value = val as Protocol, - disabled: props.disabled, - }, protocolOptions), - } - !props.disabled && (slots.append = () => h(ElButton, { - onClick: () => handlePaste( - url => { - urlRef.value = url - ctx.emit('urlChange', url) - }, - prot => protocolRef.value = prot - ) - }, () => pasteButtonText)) - + const slots_: _Slots = slots(protocolRef, urlRef, props.disabled) return h(ElInput, { modelValue: urlRef.value, clearable: !props.disabled, @@ -119,7 +125,7 @@ const _default = defineComponent({ // disabled: true, onInput: (_val: string) => { /** Do nothing */ }, placeholder - }, slots) + }, slots_) } ) } diff --git a/src/app/components/limit/modify/index.ts b/src/app/components/limit/modify/index.ts index e142d2d80..78994723b 100644 --- a/src/app/components/limit/modify/index.ts +++ b/src/app/components/limit/modify/index.ts @@ -17,7 +17,9 @@ const db = new LimitDatabase(chrome.storage.local) const noUrlError = t(msg => msg.limit.message.noUrl) const noTimeError = t(msg => msg.limit.message.noTime) const _default = defineComponent({ - emits: ['save'], + emits: { + save: (_saved: timer.limit.Rule) => true + }, setup: (_, ctx) => { const visible: Ref = ref(false) const form: Ref = ref() diff --git a/src/app/components/limit/table/column/delay.ts b/src/app/components/limit/table/column/delay.ts index 66710768e..616aa280c 100644 --- a/src/app/components/limit/table/column/delay.ts +++ b/src/app/components/limit/table/column/delay.ts @@ -15,7 +15,9 @@ const tooltip = t(msg => msg.limit.item.delayAllowedInfo) const _default = defineComponent({ name: "LimitDelayColumn", - emits: ["rowChange"], + emits: { + rowChange: (_row: timer.limit.Rule, _val: boolean) => true, + }, setup(_, ctx) { return () => h(ElTableColumn, { prop: 'delayClosed', diff --git a/src/app/components/limit/table/column/enabled.ts b/src/app/components/limit/table/column/enabled.ts index 00fbde150..60b1d0ac9 100644 --- a/src/app/components/limit/table/column/enabled.ts +++ b/src/app/components/limit/table/column/enabled.ts @@ -13,7 +13,9 @@ const label = t(msg => msg.limit.item.enabled) const _default = defineComponent({ name: "LimitEnabledColumn", - emits: ["rowChange"], + emits: { + rowChange: (_row: timer.limit.Item, _val: boolean) => true + }, setup(_, ctx) { return () => h(ElTableColumn, { prop: 'enabled', diff --git a/src/app/components/limit/table/column/operation.ts b/src/app/components/limit/table/column/operation.ts index 1fec22119..a42e68094 100644 --- a/src/app/components/limit/table/column/operation.ts +++ b/src/app/components/limit/table/column/operation.ts @@ -15,7 +15,10 @@ const deleteButtonText = t(msg => msg.limit.button.delete) const modifyButtonText = t(msg => msg.limit.button.modify) const _default = defineComponent({ name: "LimitOperationColumn", - emits: ["rowDelete", "rowModify"], + emits: { + rowDelete: (_row: timer.limit.Item, _cond: string) => true, + rowModify: (_row: timer.limit.Item) => true, + }, setup(_props, ctx) { return () => h(ElTableColumn, { prop: 'operations', diff --git a/src/app/components/limit/table/index.ts b/src/app/components/limit/table/index.ts index 8ad1c1250..07f4bd181 100644 --- a/src/app/components/limit/table/index.ts +++ b/src/app/components/limit/table/index.ts @@ -19,7 +19,12 @@ const _default = defineComponent({ props: { data: Array as PropType }, - emits: ["delayChange", "enabledChange", "delete", "modify"], + emits: { + delayChange: (_row: timer.limit.Item) => true, + enabledChange: (_row: timer.limit.Item) => true, + delete: (_row: timer.limit.Item) => true, + modify: (_row: timer.limit.Item) => true, + }, setup(props, ctx) { return () => h(ElTable, { border: true, @@ -33,13 +38,13 @@ const _default = defineComponent({ h(LimitTimeColumn), h(LimitWasteColumn), h(LimitDelayColumn, { - onRowChange: (row: timer.limit.Item, _allowDelay: boolean) => ctx.emit("delayChange", row) + onRowChange: (row: timer.limit.Item) => ctx.emit("delayChange", row) }), h(LimitEnabledColumn, { - onRowChange: (row: timer.limit.Item, _enabled: boolean) => ctx.emit("enabledChange", row) + onRowChange: (row: timer.limit.Item) => ctx.emit("enabledChange", row) }), h(LimitOperationColumn, { - onRowDelete: (row: timer.limit.Item, _cond: string) => ctx.emit("delete", row), + onRowDelete: (row: timer.limit.Item) => ctx.emit("delete", row), onRowModify: (row: timer.limit.Item) => ctx.emit("modify", row), }) ]) diff --git a/src/app/components/option/components/appearance/dark-mode-input.ts b/src/app/components/option/components/appearance/dark-mode-input.ts index 35e8ad236..0cfa67610 100644 --- a/src/app/components/option/components/appearance/dark-mode-input.ts +++ b/src/app/components/option/components/appearance/dark-mode-input.ts @@ -37,7 +37,9 @@ const _default = defineComponent({ startSecond: Number, endSecond: Number }, - emits: ["change"], + emits: { + change: (_darkMode: timer.option.DarkMode, [_startSecond, _endSecond]: [number, number]) => true + }, setup(props, ctx) { const darkMode: Ref = ref(props.modelValue) // @ts-ignore diff --git a/src/app/components/report/file-export.ts b/src/app/components/report/file-export.ts index c829201e4..89dd325aa 100644 --- a/src/app/components/report/file-export.ts +++ b/src/app/components/report/file-export.ts @@ -24,7 +24,7 @@ type _ExportInfo = { /** * Compute the name of downloaded file */ -function computeFileName(filterParam: timer.app.report.FilterOption): string { +function computeFileName(filterParam: ReportFilterOption): string { let baseName = t(msg => msg.report.exportFileName) const { dateRange, mergeDate, mergeHost, timeFormat } = filterParam if (dateRange && dateRange.length === 2) { @@ -55,13 +55,13 @@ const generateJsonData = (rows: timer.stat.Row[]) => rows.map(row => { * @param filterParam filter params * @param rows row data */ -export function exportJson(filterParam: timer.app.report.FilterOption, rows: timer.stat.Row[]): void { +export function exportJson(filterParam: ReportFilterOption, rows: timer.stat.Row[]): void { const fileName = computeFileName(filterParam) const jsonData = generateJsonData(rows) exportJson_(jsonData, fileName) } -function generateCsvData(rows: timer.stat.Row[], filterParam: timer.app.report.FilterOption): string[][] { +function generateCsvData(rows: timer.stat.Row[], filterParam: ReportFilterOption): string[][] { const { mergeDate, mergeHost } = filterParam const columnName: string[] = [] if (!mergeDate) { @@ -96,7 +96,7 @@ function generateCsvData(rows: timer.stat.Row[], filterParam: timer.app.report.F * @param filterParam filter params * @param rows row data */ -export function exportCsv(filterParam: timer.app.report.FilterOption, rows: timer.stat.Row[]): void { +export function exportCsv(filterParam: ReportFilterOption, rows: timer.stat.Row[]): void { const fileName = computeFileName(filterParam) const csvData = generateCsvData(rows, filterParam) exportCsv_(csvData, fileName) diff --git a/src/app/components/report/filter/download-file.ts b/src/app/components/report/filter/download-file.ts index 5d1521e87..c06912bc3 100644 --- a/src/app/components/report/filter/download-file.ts +++ b/src/app/components/report/filter/download-file.ts @@ -9,13 +9,13 @@ import { Download } from "@element-plus/icons-vue" import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus" import { h, defineComponent } from "vue" -export type FileFormat = 'json' | 'csv' - const ALL_FILE_FORMATS: FileFormat[] = ["json", "csv"] const _default = defineComponent({ name: "FileDownload", - emits: ["download"], + emits: { + download: (_format: FileFormat) => true, + }, setup(_, ctx) { return () => h(ElDropdown, { class: 'export-dropdown', showTimeout: 100 }, { default: () => h(ElButton, diff --git a/src/app/components/report/filter/index.ts b/src/app/components/report/filter/index.ts index 4d78d9d34..084e8b985 100644 --- a/src/app/components/report/filter/index.ts +++ b/src/app/components/report/filter/index.ts @@ -5,7 +5,6 @@ * https://opensource.org/licenses/MIT */ -import type { FileFormat } from "./download-file" import type { Ref, PropType } from "vue" import type { ElementDatePickerShortcut } from "@src/element-ui/date" import type { ReportMessage } from "@i18n/message/app/report" @@ -60,7 +59,12 @@ const _default = defineComponent({ mergeHost: Boolean, timeFormat: String as PropType }, - emits: ["change", "download", "batchDelete", 'remoteChange'], + emits: { + change: (_filterOption: ReportFilterOption) => true, + download: (_format: FileFormat) => true, + batchDelete: (_filterOption: ReportFilterOption) => true, + remoteChange: (_readRemote: boolean) => true, + }, setup(props, ctx) { const host: Ref = ref(props.host) // Don't know why the error occurred, so ignore @@ -78,7 +82,7 @@ const _default = defineComponent({ mergeDate: mergeDate.value, mergeHost: mergeHost.value, timeFormat: timeFormat.value - } as timer.app.report.FilterOption) + } as ReportFilterOption) const handleChange = () => ctx.emit("change", computeOption()) timerService.canReadRemote().then(abled => remoteSwitchVisible.value = abled) return () => [ diff --git a/src/app/components/report/filter/remote-client.ts b/src/app/components/report/filter/remote-client.ts index fccd12f9d..aaafaadef 100644 --- a/src/app/components/report/filter/remote-client.ts +++ b/src/app/components/report/filter/remote-client.ts @@ -14,7 +14,9 @@ import { defineComponent, ref, h, watch } from "vue" const _default = defineComponent({ name: "ClientSelect", - emits: ["change"], + emits: { + change: (_readRemote: boolean) => true + }, props: { visible: Boolean }, diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index ce373b8b5..0cfa258bc 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -7,8 +7,6 @@ import type { Ref, UnwrapRef, ComputedRef } from "vue" import type { TimerQueryParam } from "@service/timer-service" -import type { SortInfo } from "./table" -import type { FileFormat } from "./filter/download-file" import type { Router, RouteLocation } from "vue-router" import { computed, defineComponent, h, reactive, ref } from "vue" @@ -16,7 +14,7 @@ import { I18nKey, t } from "@app/locale" import timerService from "@service/timer-service" import whitelistService from "@service/whitelist-service" import './styles/element' -import ReportTable, { ElSortDirect } from "./table" +import ReportTable from "./table" import ReportFilter from "./filter" import Pagination from "../common/pagination" import ContentContainer from "../common/content-container" @@ -122,7 +120,7 @@ async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: bool return t(key, i18nParam) } -async function handleBatchDelete(tableEl: Ref, filterOption: timer.app.report.FilterOption, afterDelete: Function) { +async function handleBatchDelete(tableEl: Ref, filterOption: ReportFilterOption, afterDelete: Function) { const selected: timer.stat.Row[] = tableEl?.value?.getSelected?.() || [] if (!selected?.length) { ElMessage({ type: "info", message: t(msg => msg.report.batchDelete.noSelectedMsg) }) @@ -167,8 +165,8 @@ async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateR /** * Init the query parameters */ -function initQueryParam(route: RouteLocation, router: Router): [timer.app.report.FilterOption, SortInfo] { - const routeQuery: timer.app.report.QueryParam = route.query as unknown as timer.app.report.QueryParam +function initQueryParam(route: RouteLocation, router: Router): [ReportFilterOption, SortInfo] { + const routeQuery: ReportQueryParam = route.query as unknown as ReportQueryParam const { mh, ds, de, sc } = routeQuery const dateStart = ds ? new Date(Number.parseInt(ds)) : undefined const dateEnd = ds ? new Date(Number.parseInt(de)) : undefined @@ -176,7 +174,7 @@ function initQueryParam(route: RouteLocation, router: Router): [timer.app.report router.replace({ query: {} }) const now = new Date() - const filterOption: timer.app.report.FilterOption = { + const filterOption: ReportFilterOption = { host: '', dateRange: [dateStart || now, dateEnd || now], mergeDate: false, @@ -185,23 +183,23 @@ function initQueryParam(route: RouteLocation, router: Router): [timer.app.report } const sortInfo: SortInfo = { prop: sc || 'focus', - order: ElSortDirect.DESC + order: 'descending' } return [filterOption, sortInfo] } -function computeTimerQueryParam(filterOption: timer.app.report.FilterOption, sort: SortInfo): TimerQueryParam { +function computeTimerQueryParam(filterOption: ReportFilterOption, sort: SortInfo): TimerQueryParam { return { host: filterOption.host, date: filterOption.dateRange, mergeHost: filterOption.mergeHost, mergeDate: filterOption.mergeDate, sort: sort.prop, - sortOrder: sort.order === ElSortDirect.ASC ? 'ASC' : 'DESC' + sortOrder: sort.order === 'ascending' ? 'ASC' : 'DESC' } } -function copyFilterParam(newVal: timer.app.report.FilterOption, oldVal: timer.app.report.FilterOption) { +function copyFilterParam(newVal: ReportFilterOption, oldVal: ReportFilterOption) { oldVal.host = newVal.host oldVal.dateRange = newVal.dateRange oldVal.mergeDate = newVal.mergeDate @@ -215,7 +213,7 @@ const _default = defineComponent({ const route = useRoute() const router = useRouter() const [initialFilterParam, initialSort] = initQueryParam(route, router) - const filterOption: UnwrapRef = reactive(initialFilterParam) + const filterOption: UnwrapRef = reactive(initialFilterParam) const sort: UnwrapRef = reactive(initialSort) const data: Ref = ref([]) @@ -237,7 +235,7 @@ const _default = defineComponent({ mergeDate: filterOption.mergeDate, mergeHost: filterOption.mergeHost, timeFormat: filterOption.timeFormat, - onChange: (newFilterOption: timer.app.report.FilterOption) => { + onChange: (newFilterOption: ReportFilterOption) => { copyFilterParam(newFilterOption, filterOption) query() }, @@ -246,7 +244,7 @@ const _default = defineComponent({ format === 'json' && exportJson(filterOption, rows) format === 'csv' && exportCsv(filterOption, rows) }, - onBatchDelete: (filterOption: timer.app.report.FilterOption) => handleBatchDelete(tableEl, filterOption, query), + onBatchDelete: (filterOption: ReportFilterOption) => handleBatchDelete(tableEl, filterOption, query), onRemoteChange(newRemoteChange) { remoteRead.value = newRemoteChange query() diff --git a/src/app/components/report/report.d.ts b/src/app/components/report/report.d.ts new file mode 100644 index 000000000..3bc8ebd76 --- /dev/null +++ b/src/app/components/report/report.d.ts @@ -0,0 +1,42 @@ + +declare type FileFormat = 'json' | 'csv' + +declare type SortDirect = 'ascending' | 'descending' + +declare type SortInfo = { + prop: timer.stat.Dimension | 'host' + order: SortDirect +} + +declare type ReportFilterOption = { + host: string + dateRange: Date[] + mergeDate: boolean + mergeHost: boolean + /** + * @since 1.1.7 + */ + timeFormat: timer.app.TimeFormat +} + +/** +* The query param of report page +*/ +declare type ReportQueryParam = { + /** + * Merge host + */ + mh?: string + /** + * Date start + */ + ds?: string + /** + * Date end + */ + de?: string + /** + * Sorted column + */ + sc?: timer.stat.Dimension +} \ No newline at end of file diff --git a/src/app/components/report/table/columns/alias-info.ts b/src/app/components/report/table/columns/alias-info.ts index e612970f2..c54f7639a 100644 --- a/src/app/components/report/table/columns/alias-info.ts +++ b/src/app/components/report/table/columns/alias-info.ts @@ -17,9 +17,11 @@ type _Data = { input: Ref } -type _Emits = "change" +type _Emits = { + change: (_newVal: string) => true +} -function renderEditing(data: _Data, ctx: SetupContext<_Emits[]>) { +function renderEditing(data: _Data, ctx: SetupContext<_Emits>) { return h(ElInput, { size: 'small', ref: data.input, @@ -71,7 +73,7 @@ function renderText(data: _Data) { return result } -function render(data: _Data, ctx) { +function render(data: _Data, ctx: SetupContext<_Emits>) { const isEditing = data.editing.value if (isEditing) { return renderEditing(data, ctx) @@ -89,7 +91,9 @@ const _default = defineComponent({ type: String } }, - emits: ['change'], + emits: { + change: (_newAlias: string) => true + }, setup(props, ctx) { const editing = ref(false) const originVal = ref(props.modelValue) diff --git a/src/app/components/report/table/columns/alias.ts b/src/app/components/report/table/columns/alias.ts index f3c148adc..a7718f1ba 100644 --- a/src/app/components/report/table/columns/alias.ts +++ b/src/app/components/report/table/columns/alias.ts @@ -17,7 +17,9 @@ const columnLabel = t(msg => msg.siteManage.column.alias) const _default = defineComponent({ name: "AliasColumn", - emits: ["aliasChange"], + emits: { + aliasChange: (_host: string, _newAlias: string) => true, + }, setup(_, ctx) { return () => h(ElTableColumn, { label: columnLabel, diff --git a/src/app/components/report/table/columns/operation-delete-button.ts b/src/app/components/report/table/columns/operation-delete-button.ts index 398bd3914..8ffd44e54 100644 --- a/src/app/components/report/table/columns/operation-delete-button.ts +++ b/src/app/components/report/table/columns/operation-delete-button.ts @@ -4,8 +4,9 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { PropType } from "vue" -import { computed, defineComponent, h, PropType, Ref } from "vue" +import { computed, defineComponent, h, Ref } from "vue" import OperationPopupConfirmButton from "@app/components/common/popup-confirm-button" import { Delete } from "@element-plus/icons-vue" import { t } from "@app/locale" @@ -44,7 +45,9 @@ function computeRangeConfirmText(url: string, dateRange: Array): string { const _default = defineComponent({ name: "OperationDeleteButton", - emits: ["confirm"], + emits: { + confirm: () => true + }, props: { mergeDate: Boolean, // Filter of date range diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index d6ebc7eaa..526a7c5d7 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -7,10 +7,10 @@ /** * Generate operation buttons - * - * @todo !!!! Remaining code of optimizing performance */ -import { computed, defineComponent, h, PropType } from "vue" +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 whitelistService from "@service/whitelist-service" @@ -51,7 +51,10 @@ const _default = defineComponent({ dateRange: Array as PropType>, whitelist: Array as PropType> }, - emits: ["whitelistChange", "delete"], + emits: { + whitelistChange: (_host: string, _isWhite: boolean) => true, + delete: (_row: timer.stat.Row) => true, + }, setup(props, ctx) { const canOperate = computed(() => !props.mergeHost) const width = computed(() => props.mergeHost ? 110 : locale === "zh_CN" ? 290 : 330) diff --git a/src/app/components/report/table/index.ts b/src/app/components/report/table/index.ts index 740c63797..dcaaaaa14 100644 --- a/src/app/components/report/table/index.ts +++ b/src/app/components/report/table/index.ts @@ -17,16 +17,6 @@ import FocusColumn from "./columns/focus" import TimeColumn from "./columns/time" import OperationColumn from "./columns/operation" -export enum ElSortDirect { - ASC = 'ascending', - DESC = 'descending' -} - -export type SortInfo = { - prop: timer.stat.Dimension | 'host' - order: ElSortDirect -} - const _default = defineComponent({ name: "ReportTable", props: { @@ -38,7 +28,12 @@ const _default = defineComponent({ dateRange: Array as PropType, whitelist: Array as PropType }, - emits: ["sortChange", "aliasChange", "itemDelete", "whitelistChange"], + emits: { + sortChange: (_newSortInfo: SortInfo) => true, + aliasChange: (_host: string, _newAlias: string) => true, + itemDelete: (_row: timer.stat.Row) => true, + whitelistChange: (_host: string, _addOrRemove: boolean) => true, + }, setup(props, ctx) { let selectedRows: timer.stat.Row[] = [] ctx.expose({ diff --git a/src/app/components/rule-merge/components/add-button.ts b/src/app/components/rule-merge/components/add-button.ts index 930b1648a..68cf25f15 100644 --- a/src/app/components/rule-merge/components/add-button.ts +++ b/src/app/components/rule-merge/components/add-button.ts @@ -15,7 +15,9 @@ const buttonText = `+ ${t(msg => msg.operation.newOne)}` const _default = defineComponent({ name: "MergeRuleAddButton", - emits: ["saved"], + emits: { + save: (_origin: string, _merged: string | number) => true, + }, setup(_props, ctx) { const editing: Ref = ref(false) const origin: Ref = ref('') @@ -29,13 +31,13 @@ const _default = defineComponent({ ? h(ItemInput, { origin: origin.value, merged: merged.value, - onSaved: (newOrigin, newMerged) => { + onSave: (newOrigin, newMerged) => { const newMergedVal = tryParseInteger(newMerged?.trim())[1] merged.value = newMergedVal origin.value = newOrigin - ctx.emit('saved', newOrigin, newMergedVal) + ctx.emit('save', newOrigin, newMergedVal) }, - onCanceled: () => editing.value = false + onCancel: () => editing.value = false }) : h(ElButton, { size: "small", diff --git a/src/app/components/rule-merge/components/item-input.ts b/src/app/components/rule-merge/components/item-input.ts index bf601f061..7661210eb 100644 --- a/src/app/components/rule-merge/components/item-input.ts +++ b/src/app/components/rule-merge/components/item-input.ts @@ -26,7 +26,10 @@ const _default = defineComponent({ default: "" } }, - emits: ["saved", "canceled"], + emits: { + save: (_origin: string, _merged: string) => true, + cancel: () => true, + }, setup(props, ctx) { const origin: Ref = ref(props.origin) const merged: Ref = ref(props.merged?.toString()) @@ -54,14 +57,14 @@ const _default = defineComponent({ onClick: () => { origin.value = props.origin merged.value = props.merged?.toString() - ctx.emit("canceled") + ctx.emit("cancel") } }), h(ElButton, { size: 'small', icon: Check, class: 'item-check-button editable-item', - onClick: () => isValidHost(origin.value) ? ctx.emit("saved", origin.value, merged.value) : ElMessage.warning(invalidTxt) + onClick: () => isValidHost(origin.value) ? ctx.emit("save", origin.value, merged.value) : ElMessage.warning(invalidTxt) }) ]) } diff --git a/src/app/components/rule-merge/components/item.ts b/src/app/components/rule-merge/components/item.ts index 719e50009..59de3d592 100644 --- a/src/app/components/rule-merge/components/item.ts +++ b/src/app/components/rule-merge/components/item.ts @@ -35,7 +35,10 @@ const _default = defineComponent({ type: Number } }, - emits: ["changed", "deleted"], + emits: { + change: (_origin: string, _merged: string | number, _idx: number) => true, + delete: (_origin: string) => true, + }, setup(props, ctx) { const origin: Ref = ref(props.origin) watch(() => props.origin, newVal => origin.value = newVal) @@ -56,14 +59,14 @@ const _default = defineComponent({ ? h(ItemInput, { origin: origin.value, merged: merged.value, - onSaved: (newOrigin: string, newMerged: string) => { + onSave: (newOrigin: string, newMerged: string) => { origin.value = newOrigin const newMergedVal = tryParseInteger(newMerged?.trim())[1] merged.value = newMergedVal editing.value = false - ctx.emit("changed", newOrigin, newMergedVal, id.value) + ctx.emit("change", newOrigin, newMergedVal, id.value) }, - onCanceled: () => { + onCancel: () => { origin.value = props.origin merged.value = props.merged editing.value = false @@ -73,7 +76,7 @@ const _default = defineComponent({ class: 'editable-item', type: type.value, closable: true, - onClose: () => ctx.emit("deleted", origin.value) + onClose: () => ctx.emit("delete", origin.value) }, () => [`${origin.value} >>> ${txt.value}`, h(Edit, { class: "edit-icon", onclick: () => editing.value = true })]) } }) diff --git a/src/app/components/rule-merge/item-list.ts b/src/app/components/rule-merge/item-list.ts index 7d313244a..ee62cd0de 100644 --- a/src/app/components/rule-merge/item-list.ts +++ b/src/app/components/rule-merge/item-list.ts @@ -85,8 +85,8 @@ function generateTagItem(ruleItem: timer.merge.Rule, index: number): VNode { index, origin, merged, - onDeleted: origin => handleTagClose(origin), - onChanged: (origin, merged, index) => handleChange(origin, merged, index, itemRef) + onDelete: origin => handleTagClose(origin), + onChange: (origin, merged, index) => handleChange(origin, merged, index, itemRef) }) } @@ -98,7 +98,7 @@ const itemList = () => { const addButtonRef: Ref = ref() const addButton = h(AddButton, { ref: addButtonRef, - onSaved: (origin, merged) => handleInputConfirm(origin, merged, addButtonRef) + onSave: (origin, merged) => handleInputConfirm(origin, merged, addButtonRef) }) result.push(addButton) return h('div', { class: 'editable-tag-container' }, result) diff --git a/src/app/components/site-manage/filter.ts b/src/app/components/site-manage/filter.ts index 20aafeeb3..5fba88970 100644 --- a/src/app/components/site-manage/filter.ts +++ b/src/app/components/site-manage/filter.ts @@ -17,12 +17,6 @@ const aliasPlaceholder = t(msg => msg.siteManage.aliasPlaceholder) const onlyDetectedLabel = t(msg => msg.siteManage.onlyDetected) const addButtonText = t(msg => msg.siteManage.button.add) -export type SiteManageFilterOption = { - host: string, - alias: string, - onlyDetected: boolean, -} - const _default = defineComponent({ name: "SiteManageFilter", props: { @@ -30,7 +24,10 @@ const _default = defineComponent({ alias: String, onlyDetected: Boolean }, - emits: ["change", "create"], + emits: { + change: (_option: SiteManageFilterOption) => true, + create: () => true, + }, setup(props, ctx) { const host: Ref = ref(props.host) const alias: Ref = ref(props.alias) @@ -39,7 +36,7 @@ const _default = defineComponent({ host: host.value, alias: alias.value, onlyDetected: onlyDetected.value - } as SiteManageFilterOption) + }) return () => [ h(InputFilterItem, { placeholder: hostPlaceholder, diff --git a/src/app/components/site-manage/index.ts b/src/app/components/site-manage/index.ts index 92c8d37ba..f59e55e5d 100644 --- a/src/app/components/site-manage/index.ts +++ b/src/app/components/site-manage/index.ts @@ -9,7 +9,7 @@ import type { Ref, UnwrapRef, ComputedRef, WritableComputedRef } from "vue" import { reactive, defineComponent, h, ref, computed, } from "vue" import ContentContainer from "../common/content-container" -import SiteManageFilter, { SiteManageFilterOption } from "./filter" +import SiteManageFilter from "./filter" import Pagination from "../common/pagination" import SiteManageTable from "./table" import hostAliasService, { HostAliasQueryParam } from "@service/host-alias-service" 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 049317658..1bd61fbb3 100644 --- a/src/app/components/site-manage/modify/host-form-item.ts +++ b/src/app/components/site-manage/modify/host-form-item.ts @@ -63,7 +63,9 @@ const _default = defineComponent({ editing: Boolean, modelValue: Object as PropType }, - emits: ["change"], + emits: { + change: (_aliasKey: timer.site.AliasKey) => true + }, setup(props, ctx) { const searching: Ref = ref(false) const searchedHosts: Ref<_OptionInfo[]> = ref([]) diff --git a/src/app/components/site-manage/modify/index.ts b/src/app/components/site-manage/modify/index.ts index 5db12ae81..5085ec8c7 100644 --- a/src/app/components/site-manage/modify/index.ts +++ b/src/app/components/site-manage/modify/index.ts @@ -5,8 +5,10 @@ * https://opensource.org/licenses/MIT */ +import type { Ref, SetupContext, UnwrapRef } from "vue" + import { ElButton, ElDialog, ElForm, ElMessage } from "element-plus" -import { computed, defineComponent, h, reactive, ref, Ref, SetupContext, UnwrapRef } from "vue" +import { computed, 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" @@ -47,7 +49,7 @@ function validateForm(formRef: Ref): Promise { }) } -async function handleSave(ctx: SetupContext<_Emit[]>, isNew: boolean, formData: _FormData): 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)) { @@ -65,15 +67,19 @@ async function handleSave(ctx: SetupContext<_Emit[]>, isNew: boolean, formData: return true } -type _Emit = "save" +type _Emit = { + save: (isNew: boolean, aliasKey: timer.site.AliasKey, name: string) => void +} const BTN_ADD_TXT = t(msg => msg.siteManage.button.add) const BTN_MDF_TXT = t(msg => msg.siteManage.button.modify) const _default = defineComponent({ name: "HostAliasModify", - emits: ['save'], - setup: (_, context: SetupContext<_Emit[]>) => { + emits: { + save: () => true + }, + setup: (_, context: SetupContext<_Emit>) => { const isNew: Ref = ref(false) const visible: Ref = ref(false) const buttonText = computed(() => isNew ? BTN_ADD_TXT : BTN_MDF_TXT) diff --git a/src/app/components/site-manage/modify/name-form-item.ts b/src/app/components/site-manage/modify/name-form-item.ts index b635951d2..92e4ec8ed 100644 --- a/src/app/components/site-manage/modify/name-form-item.ts +++ b/src/app/components/site-manage/modify/name-form-item.ts @@ -15,7 +15,10 @@ const _default = defineComponent({ props: { modelValue: String }, - emits: ["input", "enter"], + emits: { + input: (_val: string) => true, + enter: () => true, + }, setup(props, ctx) { return () => h(ElFormItem, { prop: 'name', label: LABEL }, diff --git a/src/app/components/site-manage/site-manage.d.ts b/src/app/components/site-manage/site-manage.d.ts new file mode 100644 index 000000000..d7bfcb18e --- /dev/null +++ b/src/app/components/site-manage/site-manage.d.ts @@ -0,0 +1,5 @@ +declare type SiteManageFilterOption = { + host: string, + alias: string, + onlyDetected: boolean, +} \ 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 c7c1a2536..3aedced9b 100644 --- a/src/app/components/site-manage/table/column/operation.ts +++ b/src/app/components/site-manage/table/column/operation.ts @@ -5,14 +5,21 @@ * https://opensource.org/licenses/MIT */ +import type { SetupContext } from "vue" + import { ElButton, ElTableColumn } from "element-plus" import { t } from "@app/locale" -import { defineComponent, h, SetupContext } from "vue" +import { defineComponent, h } from "vue" import { Delete, Edit } from "@element-plus/icons-vue" import PopupConfirmButton from "@app/components/common/popup-confirm-button" +type _Emit = { + delete: (row: timer.site.AliasIcon) => void + modify: (row: timer.site.AliasIcon) => 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.AliasIcon) => h(PopupConfirmButton, { buttonIcon: Delete, buttonType: "danger", buttonText: deleteButtonText, @@ -21,20 +28,21 @@ const deleteButton = (ctx: SetupContext<_Emit[]>, row: timer.site.AliasIcon) => }) const modifyButtonText = t(msg => msg.siteManage.button.modify) -const modifyButton = (ctx: SetupContext<_Emit[]>, row: timer.site.AliasIcon) => h(ElButton, { +const modifyButton = (ctx: SetupContext<_Emit>, row: timer.site.AliasIcon) => h(ElButton, { size: 'small', type: "primary", icon: Edit, onClick: () => ctx.emit("modify", row) }, () => modifyButtonText) -type _Emit = "delete" | "modify" - const label = t(msg => msg.item.operation.label) const _default = defineComponent({ name: "OperationColumn", - emits: ["delete", "modify"], - setup(_, ctx: SetupContext<_Emit[]>) { + emits: { + delete: (_row: timer.site.AliasIcon) => true, + modify: () => true, + }, + setup(_, ctx: SetupContext<_Emit>) { return () => h(ElTableColumn, { minWidth: 100, label, diff --git a/src/app/components/site-manage/table/index.ts b/src/app/components/site-manage/table/index.ts index 9048155a0..233958677 100644 --- a/src/app/components/site-manage/table/index.ts +++ b/src/app/components/site-manage/table/index.ts @@ -17,7 +17,10 @@ const _default = defineComponent({ props: { data: Array as PropType }, - emits: ["rowDelete", "rowModify"], + emits: { + rowDelete: (_row: timer.site.AliasIcon) => true, + rowModify: (_row: timer.site.AliasIcon) => true, + }, setup(props, ctx) { return () => h(ElTable, { data: props.data, diff --git a/src/app/components/trend/components/chart/index.ts b/src/app/components/trend/components/chart/index.ts index 0b1ec72c8..1ff38457b 100644 --- a/src/app/components/trend/components/chart/index.ts +++ b/src/app/components/trend/components/chart/index.ts @@ -16,7 +16,7 @@ const _default = defineComponent({ const elRef: Ref = ref() const chartWrapper: ChartWrapper = new ChartWrapper() - function render(filterOption: timer.app.trend.FilterOption, isOnMounted: boolean, row: timer.stat.Row[]) { + function render(filterOption: TrendFilterOption, isOnMounted: boolean, row: timer.stat.Row[]) { chartWrapper.render({ ...filterOption, isFirst: isOnMounted }, row) } diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts index 1a64eedac..fc108689c 100644 --- a/src/app/components/trend/components/chart/wrapper.ts +++ b/src/app/components/trend/components/chart/wrapper.ts @@ -185,7 +185,7 @@ function getAxias(format: string, dateRange: Date[] | undefined): string[] { return xAxisData } -async function processSubtitle(host: timer.app.trend.HostInfo) { +async function processSubtitle(host: TrendHostInfo) { let subtitle = labelOfHostInfo(host) if (!subtitle) { return DEFAULT_SUB_TITLE @@ -224,7 +224,7 @@ class ChartWrapper { this.instance = init(container) } - async render(renderOption: timer.app.trend.RenderOption, rows: timer.stat.Row[]) { + async render(renderOption: TrendRenderOption, rows: timer.stat.Row[]) { const { host, dateRange, timeFormat } = renderOption // 1. x-axis data let xAxisData: string[], allDates: string[] diff --git a/src/app/components/trend/components/common.ts b/src/app/components/trend/components/common.ts index c07deb8ee..cb8ecc0a0 100644 --- a/src/app/components/trend/components/common.ts +++ b/src/app/components/trend/components/common.ts @@ -10,7 +10,7 @@ import { t } from "@app/locale" /** * Transfer host info to label */ -export function labelOfHostInfo(hostInfo: timer.app.trend.HostInfo): string { +export function labelOfHostInfo(hostInfo: TrendHostInfo): string { if (!hostInfo) return '' const { host, merged } = hostInfo if (!host) return '' diff --git a/src/app/components/trend/components/filter.ts b/src/app/components/trend/components/filter.ts index a006336d3..44a411049 100644 --- a/src/app/components/trend/components/filter.ts +++ b/src/app/components/trend/components/filter.ts @@ -18,14 +18,14 @@ import SelectFilterItem from "@app/components/common/select-filter-item" import { ElementDatePickerShortcut } from "@src/element-ui/date" import { labelOfHostInfo } from "./common" -async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { +async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { if (!queryStr) { trendDomainOptions.value = [] return } searching.value = true const domains: HostSet = await timerService.listHosts(queryStr) - const options: timer.app.trend.HostInfo[] = [] + const options: TrendHostInfo[] = [] domains.origin.forEach(host => options.push({ host, merged: false })) domains.merged.forEach(host => options.push({ host, merged: true })) trendDomainOptions.value = options @@ -57,12 +57,12 @@ const TIME_FORMAT_LABELS: { [key in timer.app.TimeFormat]: string } = { hour: t(msg => msg.timeFormat.hour) } -function keyOfHostInfo(option: timer.app.trend.HostInfo): string { +function keyOfHostInfo(option: TrendHostInfo): string { const { merged, host } = option return (merged ? "1" : '0') + (host || '') } -function hostInfoOfKey(key: string): timer.app.trend.HostInfo { +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 } @@ -72,17 +72,19 @@ const _default = defineComponent({ name: "TrendFilter", props: { dateRange: Object as PropType, - defaultValue: Object as PropType, + defaultValue: Object as PropType, timeFormat: String as PropType }, - emits: ['change'], + emits: { + change: (_option: TrendFilterOption) => true + }, setup(props, ctx) { // @ts-ignore const dateRange: Ref = ref(props.dateRange) const domainKey: Ref = ref('') const trendSearching: Ref = ref(false) - const trendDomainOptions: Ref = ref([]) - const defaultOption: timer.app.trend.HostInfo = props.defaultValue + const trendDomainOptions: Ref = ref([]) + const defaultOption: TrendHostInfo = props.defaultValue const timeFormat: Ref = ref(props.timeFormat) if (defaultOption) { domainKey.value = keyOfHostInfo(defaultOption) @@ -90,8 +92,8 @@ const _default = defineComponent({ } function handleChange() { - const hostInfo: timer.app.trend.HostInfo = hostInfoOfKey(domainKey.value) - const option: timer.app.trend.FilterOption = { + const hostInfo: TrendHostInfo = hostInfoOfKey(domainKey.value) + const option: TrendFilterOption = { host: hostInfo, dateRange: dateRange.value, timeFormat: timeFormat.value diff --git a/src/app/components/trend/index.ts b/src/app/components/trend/index.ts index 29e21adf1..287bfa5bf 100644 --- a/src/app/components/trend/index.ts +++ b/src/app/components/trend/index.ts @@ -20,7 +20,7 @@ type _Queries = { merge: '1' | '0' | undefined } -function initWithQuery(hostOption: Ref) { +function initWithQuery(hostOption: Ref) { // Process the query param const query: _Queries = useRoute().query as unknown as _Queries useRouter().replace({ query: {} }) @@ -29,7 +29,7 @@ function initWithQuery(hostOption: Ref) { host && (hostOption.value = { host, merged: merge === "1" }) } -async function query(hostOption: Ref, dateRange: Ref): Promise { +async function query(hostOption: Ref, dateRange: Ref): Promise { const hostVal = hostOption.value?.host if (!hostVal) { return [] @@ -51,7 +51,7 @@ const _default = defineComponent({ setup() { // @ts-ignore const dateRange: Ref = ref(daysAgo(7, 0)) - const hostOption: Ref = ref() + const hostOption: Ref = ref() const timeFormat: Ref = ref('default') const chart: Ref = ref() const filter: Ref = ref() @@ -60,7 +60,7 @@ const _default = defineComponent({ async function queryAndRender(isOnMounted?: boolean) { const row = await query(hostOption, dateRange) - const filterOption: timer.app.trend.FilterOption = { + const filterOption: TrendFilterOption = { host: hostOption.value, dateRange: dateRange.value, timeFormat: timeFormat.value @@ -76,7 +76,7 @@ const _default = defineComponent({ timeFormat: timeFormat.value, dateRange: dateRange.value, ref: filter, - onChange(option: timer.app.trend.FilterOption) { + onChange(option: TrendFilterOption) { hostOption.value = option.host dateRange.value = option.dateRange timeFormat.value = option.timeFormat diff --git a/src/app/components/trend/trend.d.ts b/src/app/components/trend/trend.d.ts new file mode 100644 index 000000000..40a178c0b --- /dev/null +++ b/src/app/components/trend/trend.d.ts @@ -0,0 +1,17 @@ +declare type TrendHostInfo = { + host: string + merged: boolean +} + +declare type TrendFilterOption = { + host: TrendHostInfo, + dateRange: Date[], + timeFormat: timer.app.TimeFormat +} + +declare type TrendRenderOption = TrendFilterOption & { + /** + * Whether render firstly + */ + isFirst: boolean +} \ No newline at end of file diff --git a/src/app/components/whitelist/components/add-button.ts b/src/app/components/whitelist/components/add-button.ts index fe097b5bb..6966c96c7 100644 --- a/src/app/components/whitelist/components/add-button.ts +++ b/src/app/components/whitelist/components/add-button.ts @@ -14,7 +14,9 @@ const buttonText = `+ ${t(msg => msg.operation.newOne)}` const _default = defineComponent({ name: "WhitelistAddButton", - emits: ["saved"], + emits: { + save: (_white: string) => true, + }, setup(_props, ctx) { const editing: Ref = ref(false) const white: Ref = ref('') @@ -26,11 +28,11 @@ const _default = defineComponent({ return () => editing.value ? h(ItemInput, { white: white.value, - onSaved: (newWhite) => { + onSave: (newWhite) => { white.value = newWhite - ctx.emit('saved', newWhite) + ctx.emit('save', newWhite) }, - onCanceled: () => editing.value = false + onCancel: () => editing.value = false }) : h(ElButton, { size: "small", diff --git a/src/app/components/whitelist/components/item-input.ts b/src/app/components/whitelist/components/item-input.ts index 411dae2f3..eccb96af9 100644 --- a/src/app/components/whitelist/components/item-input.ts +++ b/src/app/components/whitelist/components/item-input.ts @@ -23,7 +23,10 @@ const _default = defineComponent({ default: "" } }, - emits: ["saved", "canceled"], + emits: { + save: (_white: string) => true, + cancel: () => true, + }, setup(props, ctx) { const white: Ref = ref(props.white) return () => h('div', { class: "item-input-container" }, [ @@ -41,7 +44,7 @@ const _default = defineComponent({ class: 'item-cancel-button editable-item', onClick: () => { white.value = props.white - ctx.emit("canceled") + ctx.emit("cancel") } }), h(ElButton, { @@ -49,7 +52,7 @@ const _default = defineComponent({ icon: Check, class: 'item-check-button editable-item', onClick: () => (isRemainHost(white.value) || isValidHost(white.value)) - ? ctx.emit("saved", white.value) + ? ctx.emit("save", white.value) : ElMessage.warning(invalidTxt) }) ]) diff --git a/src/app/components/whitelist/components/item.ts b/src/app/components/whitelist/components/item.ts index 2eeeca585..8859aa2f0 100644 --- a/src/app/components/whitelist/components/item.ts +++ b/src/app/components/whitelist/components/item.ts @@ -20,7 +20,10 @@ const _default = defineComponent({ type: Number } }, - emits: ["changed", "deleted"], + emits: { + change: (_white: string, _idx: number) => true, + delete: (_white: string) => true, + }, setup(props, ctx) { const white: Ref = ref(props.white) watch(() => props.white, newVal => white.value = newVal) @@ -36,12 +39,12 @@ const _default = defineComponent({ return () => editing.value ? h(ItemInput, { white: white.value, - onSaved: (newWhite: string) => { + onSave: (newWhite: string) => { editing.value = false white.value = newWhite - ctx.emit("changed", white.value, id.value) + ctx.emit("change", white.value, id.value) }, - onCanceled: () => { + onCancel: () => { white.value = props.white editing.value = false } @@ -49,7 +52,7 @@ const _default = defineComponent({ : h(ElTag, { class: 'editable-item', closable: true, - onClose: () => ctx.emit("deleted", white.value) + onClose: () => ctx.emit("delete", white.value) }, () => [white.value, h(Edit, { class: "edit-icon", onclick: () => editing.value = true })]) } }) diff --git a/src/app/components/whitelist/item-list.ts b/src/app/components/whitelist/item-list.ts index 1339132e9..887e5cc18 100644 --- a/src/app/components/whitelist/item-list.ts +++ b/src/app/components/whitelist/item-list.ts @@ -5,9 +5,11 @@ * https://opensource.org/licenses/MIT */ +import type { Ref, VNode } from "vue" + import { ElMessage, ElMessageBox } from "element-plus" import { t } from "@app/locale" -import { h, ref, Ref, VNode } from "vue" +import { h, ref } from "vue" import whitelistService from "@service/whitelist-service" import Item from './components/item' import AddButton from './components/add-button' @@ -72,15 +74,15 @@ function tags(): VNode { const itemRef: Ref = ref() const item = h(Item, { white, index, ref: itemRef, - onChanged: (newVal, index) => handleChanged(newVal, index, itemRef), - onDeleted: (val: string) => handleClose(val) + onChange: (newVal, index) => handleChanged(newVal, index, itemRef), + onDelete: (val: string) => handleClose(val) }) result.push(item) }) const addButtonRef: Ref = ref() result.push(h(AddButton, { ref: addButtonRef, - onSaved: (inputVal: string) => handleAdd(inputVal, addButtonRef) + onSave: (inputVal: string) => handleAdd(inputVal, addButtonRef) })) return h('div', { class: 'editable-tag-container' }, result) } diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 726b65c62..21db6fd1c 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -12,7 +12,6 @@ import { ElIcon, ElMenu, ElMenuItem, ElSubMenu } from "element-plus" import { defineComponent, h } from "vue" import { User, Memo, MagicStick } from "@element-plus/icons-vue" - type _Item = { title: I18nKey position: Position @@ -91,7 +90,9 @@ function renderGroup(handleClick: (position: Position) => void, group: _Group): const _default = defineComponent({ name: "GuideMenu", - emits: ['click'], + emits: { + click: (_position: Position) => true, + }, setup(_, ctx) { const handleClick = (position: Position) => ctx.emit('click', position) const menuItems = () => [ diff --git a/src/popup/components/chart/click-handler.ts b/src/popup/components/chart/click-handler.ts index 945a2d7ff..a1ddfbfb3 100644 --- a/src/popup/components/chart/click-handler.ts +++ b/src/popup/components/chart/click-handler.ts @@ -15,7 +15,7 @@ function generateUrl(data: timer.popup.Row, queryResult: timer.popup.QueryResult if (!isOther) { return host ? `http://${host}` : undefined } - const query: timer.app.report.QueryParam = {} + const query: ReportQueryParam = {} // Merge host queryResult.mergeHost && (query.mh = "1") // Date From adf01b2dc7ee5992411555e1454ee1f976a24019 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 12:38:09 +0800 Subject: [PATCH 082/168] Refactor code --- global.d.ts | 22 ++----------------- src/app/components/option/components/popup.ts | 2 +- src/i18n/message/common/popup-duration.ts | 2 +- src/i18n/message/popup/chart.ts | 2 +- src/popup/components/chart/click-handler.ts | 6 ++--- src/popup/components/chart/index.ts | 4 ++-- src/popup/components/chart/option.ts | 14 +++++++----- src/popup/components/footer/index.ts | 16 ++++++++------ .../components/footer/select/time-select.ts | 12 +++++----- src/popup/popup.d.ts | 13 +++++++++++ src/util/constant/popup.ts | 2 +- 11 files changed, 48 insertions(+), 47 deletions(-) create mode 100644 src/popup/popup.d.ts diff --git a/global.d.ts b/global.d.ts index 88625ada1..f1bcb7e6b 100644 --- a/global.d.ts +++ b/global.d.ts @@ -6,7 +6,7 @@ */ declare namespace timer { namespace option { - + type PopupDuration = "today" | "thisWeek" | "thisMonth" /** * Options used for the popup page */ @@ -23,7 +23,7 @@ declare namespace timer { * The default duration to search * @since 0.6.0 */ - defaultDuration: popup.Duration + 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 @@ -387,24 +387,6 @@ declare namespace timer { } } - namespace popup { - type Duration = "today" | "thisWeek" | "thisMonth" - type Row = timer.stat.Row & { isOther?: boolean } - type QueryResult = { - type: timer.stat.Dimension - mergeHost: boolean - data: Row[] - // Filter items - chartTitle: string - date: Date | Date[] - dateLength: number - } - type QueryResultHandler = (result: QueryResult) => void - type ChartProps = QueryResult & { - displaySiteName: boolean - } - } - namespace app { /** * @since 1.1.7 diff --git a/src/app/components/option/components/popup.ts b/src/app/components/option/components/popup.ts index e14c971cb..3887978ed 100644 --- a/src/app/components/option/components/popup.ts +++ b/src/app/components/option/components/popup.ts @@ -53,7 +53,7 @@ const durationSelect = (option: UnwrapRef) => h(ElSele modelValue: option.defaultDuration, size: 'small', style: { width: t(msg => msg.option.popup.durationWidth) }, - onChange: (val: timer.popup.Duration) => { + onChange: (val: PopupDuration) => { option.defaultDuration = val optionService.setPopupOption(unref(option)) } diff --git a/src/i18n/message/common/popup-duration.ts b/src/i18n/message/common/popup-duration.ts index 2a3a59900..746f9d3cf 100644 --- a/src/i18n/message/common/popup-duration.ts +++ b/src/i18n/message/common/popup-duration.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -export type PopupDurationMessage = { [key in timer.popup.Duration]: string } +export type PopupDurationMessage = { [key in timer.option.PopupDuration]: string } const _default: Messages = { zh_CN: { diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index 38cd104bf..fba3d2972 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -6,7 +6,7 @@ */ export type ChartMessage = { - title: { [key in timer.popup.Duration]: string } + title: { [key in PopupDuration]: string } mergeHostLabel: string fileName: string saveAsImageTitle: string diff --git a/src/popup/components/chart/click-handler.ts b/src/popup/components/chart/click-handler.ts index a1ddfbfb3..fb5980990 100644 --- a/src/popup/components/chart/click-handler.ts +++ b/src/popup/components/chart/click-handler.ts @@ -10,7 +10,7 @@ import type { CallbackDataParams } from "echarts/types/dist/shared" import { REPORT_ROUTE } from "@app/router/constants" import { getAppPageUrl } from "@util/constant/url" -function generateUrl(data: timer.popup.Row, queryResult: timer.popup.QueryResult): string { +function generateUrl(data: PopupRow, queryResult: PopupQueryResult): string { const { host, isOther } = data if (!isOther) { return host ? `http://${host}` : undefined @@ -37,8 +37,8 @@ function generateUrl(data: timer.popup.Row, queryResult: timer.popup.QueryResult return getAppPageUrl(false, REPORT_ROUTE, query) } -function handleClick(params: CallbackDataParams, queryResult: timer.popup.QueryResult) { - const data: timer.popup.Row = params.data as timer.popup.Row +function handleClick(params: CallbackDataParams, queryResult: PopupQueryResult) { + const data: PopupRow = params.data as PopupRow if (!data) { return } diff --git a/src/popup/components/chart/index.ts b/src/popup/components/chart/index.ts index 746bb1e4a..cf0f9417d 100644 --- a/src/popup/components/chart/index.ts +++ b/src/popup/components/chart/index.ts @@ -45,9 +45,9 @@ export const handleRestore = (handler: () => void) => { } // Store -let _queryResult: timer.popup.QueryResult +let _queryResult: PopupQueryResult -function renderChart(queryResult: timer.popup.QueryResult) { +function renderChart(queryResult: PopupQueryResult) { _queryResult = queryResult pie.setOption(pieOptions({ ...queryResult, displaySiteName }, chartContainer), true, false) } diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index 2864a865a..7d2fdfe8c 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -31,6 +31,10 @@ type EcOption = ComposeOption< | LegendComponentOption > +type ChartProps = PopupQueryResult & { + displaySiteName: boolean +} + const today = formatTime(new Date(), '{y}_{m}_{d}') /** @@ -56,10 +60,10 @@ function calculateAverageText(type: timer.stat.Dimension, averageValue: number): return undefined } -function toolTipFormatter({ type, dateLength }: timer.popup.QueryResult, params: any): string { +function toolTipFormatter({ type, dateLength }: PopupQueryResult, params: any): string { const format = params instanceof Array ? params[0] : params const { name, value, percent } = format - const data = format.data as timer.popup.Row + const data = format.data as PopupRow const host = data.host const siteLabel = generateSiteLabel(host, name) let result = siteLabel @@ -75,10 +79,10 @@ function toolTipFormatter({ type, dateLength }: timer.popup.QueryResult, params: return result } -function labelFormatter({ mergeHost }: timer.popup.QueryResult, params: any): string { +function labelFormatter({ mergeHost }: PopupQueryResult, params: any): string { const format = params instanceof Array ? params[0] : params const { name } = format - const data = format.data as timer.popup.Row + const data = format.data as PopupRow // Un-supported to get favicon url in Safari return mergeHost || data.isOther || IS_SAFARI ? name @@ -147,7 +151,7 @@ function calculateSubTitleText(date: Date | Date[]) { } } -export function pieOptions(props: timer.popup.ChartProps, container: HTMLDivElement): EcOption { +export function pieOptions(props: ChartProps, container: HTMLDivElement): EcOption { const { type, data, displaySiteName, chartTitle, date } = props const titleText = chartTitle const subTitleText = `${calculateSubTitleText(date)} @ ${t(msg => msg.meta.name)}` diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index bdeb94537..75bd372ac 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -25,9 +25,11 @@ type FooterParam = TimerQueryParam & { chartTitle: string } +type QueryResultHandler = (result: PopupQueryResult) => void + const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: !IS_SAFARI, alias: true } -function calculateDateRange(duration: timer.popup.Duration, weekStart: timer.option.WeekStartOption): Date | Date[] { +function calculateDateRange(duration: PopupDuration, weekStart: timer.option.WeekStartOption): Date | Date[] { const now = new Date() if (duration == 'today') { return now @@ -59,13 +61,13 @@ function calculateDateRange(duration: timer.popup.Duration, weekStart: timer.opt } class FooterWrapper { - private afterQuery: timer.popup.QueryResultHandler + private afterQuery: QueryResultHandler private timeSelectWrapper: TimeSelectWrapper private typeSelectWrapper: TypeSelectWrapper private mergeHostWrapper: MergeHostWrapper private totalInfoWrapper: TotalInfoWrapper - constructor(afterQuery: timer.popup.QueryResultHandler) { + constructor(afterQuery: QueryResultHandler) { this.afterQuery = afterQuery } @@ -93,8 +95,8 @@ class FooterWrapper { const itemCount = option.popupMax const queryParam = this.getQueryParam(option.weekStart) const rows = await timerService.select(queryParam, FILL_FLAG_PARAM) - const popupRows: timer.popup.Row[] = [] - const other: timer.popup.Row = { + const popupRows: PopupRow[] = [] + const other: PopupRow = { host: t(msg => msg.chart.otherLabel, { count: 0 }), focus: 0, date: '0000-00-00', @@ -118,7 +120,7 @@ class FooterWrapper { const data = popupRows.filter(item => item[type]) const date = queryParam.date - const queryResult: timer.popup.QueryResult = { + const queryResult: PopupQueryResult = { data, mergeHost: queryParam.mergeHost, type, @@ -131,7 +133,7 @@ class FooterWrapper { } getQueryParam(weekStart: timer.option.WeekStartOption): FooterParam { - const duration: timer.popup.Duration = this.timeSelectWrapper.getSelectedTime() + const duration: PopupDuration = this.timeSelectWrapper.getSelectedTime() const param: FooterParam = { date: calculateDateRange(duration, weekStart), mergeHost: this.mergeHostWrapper.mergedHost(), diff --git a/src/popup/components/footer/select/time-select.ts b/src/popup/components/footer/select/time-select.ts index 8cbe74554..41662e823 100644 --- a/src/popup/components/footer/select/time-select.ts +++ b/src/popup/components/footer/select/time-select.ts @@ -15,17 +15,17 @@ class TimeSelectWrapper { private timeSelectPopup: HTMLElement private timeSelectInput: HTMLInputElement private isOpen: boolean = false - private currentSelected: timer.popup.Duration = undefined + private currentSelected: PopupDuration = undefined private handleSelected: Function private optionList: HTMLElement - private optionItems: Map = new Map() + private optionItems: Map = new Map() constructor(handleSelected: Function) { this.handleSelected = handleSelected } - async init(initialVal: timer.popup.Duration): Promise { + async init(initialVal: PopupDuration): Promise { this.timeSelect = document.getElementById('time-select-container') this.timeSelectPopup = document.getElementById('time-select-popup') this.timeSelectInput = document.getElementById('time-select-input') as HTMLInputElement @@ -38,7 +38,7 @@ class TimeSelectWrapper { this.selected(initialVal) } - private initOption(item: timer.popup.Duration) { + private initOption(item: PopupDuration) { const li = document.createElement('li') li.classList.add('el-select-dropdown__item') li.innerText = t(msg => msg.duration[item]) @@ -51,7 +51,7 @@ class TimeSelectWrapper { this.optionItems.set(item, li) } - private selected(item: timer.popup.Duration) { + private selected(item: PopupDuration) { this.currentSelected = item Array.from(this.optionItems.values()).forEach(item => item.classList.remove(SELECTED_CLASS)) this.optionItems.get(item).classList.add(SELECTED_CLASS) @@ -70,7 +70,7 @@ class TimeSelectWrapper { this.isOpen = false } - getSelectedTime(): timer.popup.Duration { + getSelectedTime(): PopupDuration { return this.currentSelected } } diff --git a/src/popup/popup.d.ts b/src/popup/popup.d.ts new file mode 100644 index 000000000..ebf7073ae --- /dev/null +++ b/src/popup/popup.d.ts @@ -0,0 +1,13 @@ +type PopupRow = timer.stat.Row & { isOther?: boolean } + +type PopupQueryResult = { + type: timer.stat.Dimension + mergeHost: boolean + data: PopupRow[] + // Filter items + chartTitle: string + date: Date | Date[] + dateLength: number +} + +type PopupDuration = timer.option.PopupDuration \ No newline at end of file diff --git a/src/util/constant/popup.ts b/src/util/constant/popup.ts index 3bd45df96..058f66d35 100644 --- a/src/util/constant/popup.ts +++ b/src/util/constant/popup.ts @@ -5,4 +5,4 @@ * https://opensource.org/licenses/MIT */ -export const ALL_POPUP_DURATION: timer.popup.Duration[] = ["today", "thisWeek", "thisMonth"] \ No newline at end of file +export const ALL_POPUP_DURATION: PopupDuration[] = ["today", "thisWeek", "thisMonth"] \ No newline at end of file From 91a891961247d2153783e5724ba81bef3a1b4f41 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 12:41:39 +0800 Subject: [PATCH 083/168] Fix alert text --- src/i18n/message/app/data-manage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/message/app/data-manage.ts b/src/i18n/message/app/data-manage.ts index 726cc5e14..91023047f 100644 --- a/src/i18n/message/app/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -35,7 +35,7 @@ const _default: Messages = { totalMemoryAlert: '浏览器为每个扩展提供 {size}MB 来存储本地数据', totalMemoryAlert1: '无法确定浏览器允许的最大可用内存', usedMemoryAlert: '当前已使用 {size}MB', - operationAlert: '您可以归档或者删除那些无关紧要的数据,来减小内存空间', + operationAlert: '您可以删除那些无关紧要的数据,来减小内存空间', filterItems: '数据筛选', filterFocus: '当日阅览时间在 {start} 秒至 {end} 秒之间。', filterTime: '当日打开次数在 {start} 次至 {end} 次之间。', @@ -59,7 +59,7 @@ const _default: Messages = { totalMemoryAlert: '瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據', totalMemoryAlert1: '無法確定瀏覽器允許的最大可用內存', usedMemoryAlert: '當前已使用 {size}MB', - operationAlert: '您可以歸檔或者刪除那些無關緊要的數據,來減小內存空間', + operationAlert: '您可以刪除那些無關緊要的數據,來減小內存空間', filterItems: '數據篩選', filterFocus: '當日閱覽時間在 {start} 秒至 {end} 秒之間。', filterTime: '當日打開次數在 {start} 次至 {end} 次之間。', @@ -107,7 +107,7 @@ const _default: Messages = { totalMemoryAlert: 'ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します', totalMemoryAlert1: 'ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません', usedMemoryAlert: '現在 {size}MB が使用されています', - operationAlert: '不要なデータを削除してメモリ容量を減らすことができます', + operationAlert: 'これらの重要でないデータを削除して、メモリ使用量を減らすことができます', filterItems: 'データフィルタリング', filterFocus: '当日の閲覧時間は、{start} 秒から {end} 秒の間です。', filterTime: '当日のオープン数は {start} から {end} の間です。', From 673c2be0a395548b4a28cb53fd7eee72a77d97de Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 13:58:17 +0800 Subject: [PATCH 084/168] Optimize styles --- src/app/components/help-us/style.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/help-us/style.sass b/src/app/components/help-us/style.sass index 1acd46db9..29e070dc7 100644 --- a/src/app/components/help-us/style.sass +++ b/src/app/components/help-us/style.sass @@ -18,6 +18,7 @@ top: 35% color: var(--el-text-color-regular) padding-left: 15px + text-align: right .language-name,.progress-text display: block font-size: 12px From 54836504844ef6f8065a52eda9bc6354dd3c0470 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 14:12:48 +0800 Subject: [PATCH 085/168] Refresh data when the window become visible (#181) --- src/app/components/limit/index.ts | 3 +++ src/app/components/report/index.ts | 3 +++ src/util/window.ts | 12 ++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 src/util/window.ts diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index 6813a0762..1e0446f5b 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -15,6 +15,7 @@ import limitService from "@service/limit-service" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" import { ElMessage } from "element-plus" +import { handleWindowVisibleChange } from "@util/window" const _default = defineComponent({ name: "Limit", @@ -28,6 +29,8 @@ const _default = defineComponent({ data.value = list } queryData() + // Query data if the window become visible + handleWindowVisibleChange(queryData) // Init with url parameter const urlParam = useRoute().query['url'] as string useRouter().replace({ query: {} }) diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index 0cfa258bc..61a7906ff 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -26,6 +26,7 @@ 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 { handleWindowVisibleChange } from "@util/window" const timerDatabase = new TimerDatabase(chrome.storage.local) @@ -225,6 +226,8 @@ const _default = defineComponent({ const tableEl: Ref = ref() const query = () => queryData(queryParam, data, page, remoteRead) + // Query data if window become visible + handleWindowVisibleChange(query) // Init first queryWhiteList(whitelist).then(query) diff --git a/src/util/window.ts b/src/util/window.ts new file mode 100644 index 000000000..00c64595e --- /dev/null +++ b/src/util/window.ts @@ -0,0 +1,12 @@ +import { onMounted, onUnmounted } from "vue" + +/** + * handle window visible change + * + * @since 1.4.4 + */ +export function handleWindowVisibleChange(handler: () => void) { + const hanlderInner = () => document.visibilityState === 'visible' && handler() + onMounted(() => document.addEventListener('visibilitychange', hanlderInner)) + onUnmounted(() => document.removeEventListener('visibilitychange', hanlderInner)) +} From 0b6977a1003307cae65b8241342d39004cd7aa69 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 14:14:28 +0800 Subject: [PATCH 086/168] v1.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 61bd786af..c944f78f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.3", + "version": "1.4.4", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From 7c148b5b1a7aaf8701fb6423c362aa19235c762a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 20:48:48 +0800 Subject: [PATCH 087/168] Fix bugs --- src/app/components/common/switch-filter-item.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/components/common/switch-filter-item.ts b/src/app/components/common/switch-filter-item.ts index 7a3fb573f..6f17c6741 100644 --- a/src/app/components/common/switch-filter-item.ts +++ b/src/app/components/common/switch-filter-item.ts @@ -18,11 +18,22 @@ type _Props = { label: string } -const _default = defineComponent<_Props>({ +const _default = defineComponent({ name: "SwitchFilterItem", emits: { change: (_val: boolean) => true }, + props: { + label: String, + defaultValue: { + type: Boolean, + required: false, + }, + historyName: { + type: String, + required: false, + } + }, setup(props, ctx) { const modelValue: Ref = ref(props.defaultValue || false) const historyWrapper = new FilterItemHistoryWrapper(useRoute().path, props.historyName) From 0f47ab942c71b6cde9f23ab7ad1f1753612a80fb Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 14 Jan 2023 20:49:07 +0800 Subject: [PATCH 088/168] v1.4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c944f78f9..513282761 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.4", + "version": "1.4.5", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From a6a03f6a22a20ad20b2c316b8486cd29f7238b8d Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 16 Jan 2023 02:54:26 +0000 Subject: [PATCH 089/168] Refactor chrome.alarms API --- src/background/alarm-manager.ts | 62 ++++++++++++++++++++++++++++ src/background/badge-text-manager.ts | 17 ++------ src/background/timer/index.ts | 25 ++--------- 3 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 src/background/alarm-manager.ts diff --git a/src/background/alarm-manager.ts b/src/background/alarm-manager.ts new file mode 100644 index 000000000..0a6db16d5 --- /dev/null +++ b/src/background/alarm-manager.ts @@ -0,0 +1,62 @@ +type _AlarmConfig = { + handler: _Handler, + interval: number, +} + +type _Handler = (alarm: chrome.alarms.Alarm) => void + +const ALARM_PREFIX = 'timer-alarm-' + chrome.runtime.id + '-' +const ALARM_PREFIX_LENGTH = ALARM_PREFIX.length + +/** + * The manager of alarms + * + * @since 1.4.6 + */ +class AlarmManager { + private alarms: Record = {} + + constructor() { + this.init() + } + + private init() { + chrome.alarms.onAlarm.addListener(alarm => { + const name = alarm.name + if (!name.startsWith(ALARM_PREFIX)) { + // Unknown alarm + return + } + const realName = name.substring(ALARM_PREFIX_LENGTH) + const config: _AlarmConfig = this.alarms[realName] + if (!config) { + // Not register, or unregistered + return + } + // Handle alarm event + config.handler?.(alarm) + // Clear this one + chrome.alarms.clear(name) + // Create new one + chrome.alarms.create(name, { when: Date.now() + config.interval }) + }) + } + + setInterval(name: string, interval: number, handler: _Handler): void { + if (!interval || !handler) { + return + } + const config: _AlarmConfig = { handler, interval } + if (this.alarms[name]) { + // Existed, only update the config + this.alarms[name] = config + return + } + // Initialize config + this.alarms[name] = config + // Create new one alarm + chrome.alarms.create(ALARM_PREFIX + name, { when: Date.now() + interval }) + } +} + +export default new AlarmManager() \ No newline at end of file diff --git a/src/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 4991bd21d..52742a176 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -9,6 +9,7 @@ import TimerDatabase from "@db/timer-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) @@ -96,29 +97,17 @@ async function updateFocus(badgeLocation?: BadgeLocation, lastLocation?: BadgeLo return badgeLocation } -const ALARM_NAME = 'timer-badge-text-manager-alarm' -const ALARM_INTERVAL = 1000 -function createAlarm(beforeAction?: () => void) { - beforeAction?.() - chrome.alarms.create(ALARM_NAME, { when: Date.now() + ALARM_INTERVAL }) -} - class BadgeTextManager { isPaused: boolean lastLocation: BadgeLocation async init() { - createAlarm() - chrome.alarms.onAlarm.addListener(alarm => { - if (ALARM_NAME === alarm.name) { - createAlarm(() => !this.isPaused && updateFocus()) - } - }) - const option: Partial = await optionService.getAllOption() this.pauseOrResumeAccordingToOption(!!option.displayBadgeText) optionService.addOptionChangeListener(({ displayBadgeText }) => this.pauseOrResumeAccordingToOption(displayBadgeText)) whitelistHolder.addPostHandler(updateFocus) + + alarmManager.setInterval('badage-text-manager', 1000, () => !this.isPaused && updateFocus()) } /** diff --git a/src/background/timer/index.ts b/src/background/timer/index.ts index 025b19a37..14b63c800 100644 --- a/src/background/timer/index.ts +++ b/src/background/timer/index.ts @@ -10,11 +10,7 @@ import IdleListener from "./idle-listener" import save from "./save" import TimerContext from "./context" import CollectionContext from "./collection-context" - -function createAlarm(name: string, interval: number, beforeAction?: () => void) { - beforeAction?.() - chrome.alarms.create(name, { when: Date.now() + interval }) -} +import alarmManager from "../alarm-manager" /** * Timer @@ -26,23 +22,8 @@ class Timer { new IdleListener(timerContext).listen() const collector = new TimeCollector(collectionContext) - const collectAlarmName = 'timer-collect-alarm' - const saveAlarmName = 'timer-save-alarm' - createAlarm(collectAlarmName, 1000) - createAlarm(saveAlarmName, 500) - chrome.alarms.onAlarm.addListener(alarm => { - if (collectAlarmName === alarm.name) { - createAlarm(collectAlarmName, 1000, () => { - collector.collect() - chrome.alarms.clear(collectAlarmName) - }) - } else if (saveAlarmName === alarm.name) { - createAlarm(saveAlarmName, 500, () => { - save(collectionContext) - chrome.alarms.clear(saveAlarmName) - }) - } - }) + alarmManager.setInterval('collect', 1000, () => collector.collect()) + alarmManager.setInterval('save', 500, () => save(collectionContext)) } } From 3cf9ac482dffd1a3bec90441ff0d69baef5b13f2 Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 16 Jan 2023 03:19:51 +0000 Subject: [PATCH 090/168] Fix icon bugs --- src/background/badge-text-manager.ts | 30 +++++++++++++--------------- src/util/pattern.ts | 2 ++ test/util/pattern.test.ts | 1 + 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 52742a176..5c993e6b3 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -56,23 +56,21 @@ function findFocusedWindow(): Promise { ) } -function findActiveTab(): Promise { - return new Promise(resolve => findFocusedWindow().then(window => { - if (!window) { +async function findActiveTab(): Promise { + const window = await findFocusedWindow() + 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) - return + } else { + const { url, id } = tabs[0] + resolve({ tabId: id, url }) } - 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 }) - } - }) })) } @@ -116,7 +114,7 @@ class BadgeTextManager { async pause() { this.isPaused = true const tab = await findActiveTab() - setBadgeText('P', tab?.tabId) + setBadgeText('', tab?.tabId) } /** diff --git a/src/util/pattern.ts b/src/util/pattern.ts index 1a8953ef3..ea3910ade 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -18,6 +18,8 @@ export function isBrowserUrl(url: string) { // Firefox addons' pages || /^moz-extension:/.test(url) || /^edge.*?:\/\/.*$/.test(url) + // Edge extensions' pages + || /^extension:/.test(url) || /^safari.*?:\/\/.*/.test(url) } diff --git a/test/util/pattern.test.ts b/test/util/pattern.test.ts index 0dda1e930..64bc57df8 100644 --- a/test/util/pattern.test.ts +++ b/test/util/pattern.test.ts @@ -9,6 +9,7 @@ test('browser url', () => { expect(isBrowserUrl('about:addons')).toBeTruthy() // edge expect(isBrowserUrl('edge://extensions/')).toBeTruthy() + expect(isBrowserUrl('extension://ifckodfehjfpfddhjhpejmidkhelbnpa/static/app.html#/additional/option')).toBeTruthy() expect(isBrowserUrl('https://www.jss.com.cn/')).toBeFalsy() }) From 636a6791cc72cd081fe6454107ef29bff5de1267 Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 16 Jan 2023 10:00:04 +0000 Subject: [PATCH 091/168] v1.4.6.chrome.mv3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 513282761..a4de4d507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.5", + "version": "1.4.6", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From e9b011158c8f0347868fe4340eb23d24849cab22 Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 17 Jan 2023 03:46:08 +0000 Subject: [PATCH 092/168] Optimize ui of select options --- src/app/components/site-manage/common.ts | 4 ++-- .../site-manage/modify/host-form-item.ts | 23 +++++++++++++++---- src/app/components/trend/components/filter.ts | 21 ++++++++++++++--- src/app/styles/compatible.sass | 8 +++++++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/app/components/site-manage/common.ts b/src/app/components/site-manage/common.ts index 049f3b37b..ee137db5b 100644 --- a/src/app/components/site-manage/common.ts +++ b/src/app/components/site-manage/common.ts @@ -24,8 +24,8 @@ export function aliasKeyOf(value: string): timer.site.AliasKey { return { host, merged } } -const EXIST_MSG = t(msg => msg.siteManage.msg.existedTag) -const MERGED_MSG = t(msg => msg.siteManage.msg.mergedTag) +export const EXIST_MSG = t(msg => msg.siteManage.msg.existedTag) +export const MERGED_MSG = t(msg => msg.siteManage.msg.mergedTag) /** * Calclate the label of alias key to display 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 1bd61fbb3..20808923f 100644 --- a/src/app/components/site-manage/modify/host-form-item.ts +++ b/src/app/components/site-manage/modify/host-form-item.ts @@ -5,14 +5,14 @@ * https://opensource.org/licenses/MIT */ -import type { PropType, Ref } from "vue" +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 } from "element-plus" +import { ElFormItem, ElInput, ElOption, ElSelect, ElTag } from "element-plus" import { defineComponent, h, ref } from "vue" -import { aliasKeyOf, labelOf, optionValueOf } from "../common" +import { aliasKeyOf, EXIST_MSG, labelOf, MERGED_MSG, optionValueOf } from "../common" import { ALL_HOSTS, MERGED_HOST } from "@util/constant/remain-host" const hostAliasDatabase = new HostAliasDatabase(chrome.storage.local) @@ -51,9 +51,22 @@ async function handleRemoteSearch(query: string, searching: Ref, search searching.value = false } +function renderOptionSlots(aliasKey: timer.site.AliasKey, hasAlias: boolean): VNode[] { + const { host, merged } = aliasKey + 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)) + return result +} + function renderOption({ aliasKey, hasAlias }: _OptionInfo) { - let label = labelOf(aliasKey, hasAlias) - return h(ElOption, { value: optionValueOf(aliasKey), disabled: hasAlias, label }) + return h(ElOption, { + value: optionValueOf(aliasKey), + disabled: hasAlias, + label: labelOf(aliasKey, hasAlias) + }, () => renderOptionSlots(aliasKey, hasAlias)) } const HOST_LABEL = t(msg => msg.siteManage.column.host) diff --git a/src/app/components/trend/components/filter.ts b/src/app/components/trend/components/filter.ts index 44a411049..438876d07 100644 --- a/src/app/components/trend/components/filter.ts +++ b/src/app/components/trend/components/filter.ts @@ -5,9 +5,9 @@ * https://opensource.org/licenses/MIT */ -import type { Ref, PropType } from "vue" +import type { Ref, PropType, VNode } from "vue" -import { ElOption, ElSelect } from "element-plus" +import { ElOption, ElSelect, ElTag } from "element-plus" import { ref, h, defineComponent } from "vue" import timerService, { HostSet } from "@service/timer-service" import { daysAgo } from "@util/time" @@ -38,6 +38,7 @@ function datePickerShortcut(msg: keyof TrendMessage, agoOfStart?: number, agoOfE value: daysAgo(agoOfStart || 0, agoOfEnd || 0) } } + const SHORTCUTS = [ datePickerShortcut('lastWeek', 7), datePickerShortcut('last15Days', 15), @@ -68,6 +69,17 @@ function hostInfoOfKey(key: string): TrendHostInfo { return { host: key.substring(1), merged } } +const MERGED_TAG_TXT = t(msg => msg.trend.merged) +function renderHostLabel(hostInfo: TrendHostInfo): VNode[] { + const result = [ + h('span', {}, hostInfo.host) + ] + hostInfo.merged && result.push( + h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) + ) + return result +} + const _default = defineComponent({ name: "TrendFilter", props: { @@ -119,7 +131,10 @@ const _default = defineComponent({ handleChange() } }, () => (trendDomainOptions.value || [])?.map( - hostInfo => h(ElOption, { value: keyOfHostInfo(hostInfo), label: labelOfHostInfo(hostInfo) }) + hostInfo => h(ElOption, { + value: keyOfHostInfo(hostInfo), + label: labelOfHostInfo(hostInfo), + }, () => renderHostLabel(hostInfo)) )), h(DateRangeFilterItem, { defaultRange: dateRange.value, diff --git a/src/app/styles/compatible.sass b/src/app/styles/compatible.sass index 03df3b636..58f1ad715 100644 --- a/src/app/styles/compatible.sass +++ b/src/app/styles/compatible.sass @@ -38,6 +38,14 @@ .el-select input padding-left: 11px +// The tag in select options +.el-select-dropdown__item + .el-tag--small + height: 20px !important + margin-right: 0px !important + margin-left: 7px + text-align: center + font-size: 10px \:root --el-input-height: 40px From 65aebed00fc10699dd2810ed17897d88a6bb6a4d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 18 Jan 2023 20:47:51 +0800 Subject: [PATCH 093/168] Optmize limit messages (#183) --- global.d.ts | 2 +- src/app/components/limit/index.ts | 2 +- src/content-script/limit.ts | 18 ++++++------ src/service/limit-service.ts | 47 +++++++++++++++++-------------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/global.d.ts b/global.d.ts index f1bcb7e6b..c82303aa6 100644 --- a/global.d.ts +++ b/global.d.ts @@ -470,7 +470,7 @@ declare namespace timer { // @since 0.9.0 | 'limitWaking' // @since 1.2.3 - | 'limitRemoved' + | 'limitChanged' // Request by content script // @since 1.3.0 | "cs.isInWhitelist" diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index 1e0446f5b..bb7f7dc9b 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -55,7 +55,7 @@ const _default = defineComponent({ h(LimitTable, { data: data.value, onDelayChange: (row: timer.limit.Item) => limitService.updateDelay(row), - onEnabledChange: (row: timer.limit.Item) => limitService.update(row), + onEnabledChange: (row: timer.limit.Item) => limitService.updateEnabled(row), async onDelete(row: timer.limit.Item) { await limitService.remove(row) ElMessage.success(t(msg => msg.limit.message.deleted)) diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts index 44d81cd8b..282dd9631 100644 --- a/src/content-script/limit.ts +++ b/src/content-script/limit.ts @@ -78,6 +78,8 @@ class _Modal { } this.delayContainer.append(link) this.mask.append(this.delayContainer) + } else if (!showDelay && this.mask.childElementCount === 2) { + this.mask.children?.[1]?.remove?.() } if (this.visible) { return @@ -195,15 +197,15 @@ function handleLimitWaking(msg: timer.mq.Request, modal: _Mo return { code: "success" } } -function handleLimitRemoved(msg: timer.mq.Request, modal: _Modal): timer.mq.Response { - if (msg.code !== 'limitRemoved') { +function handleLimitChanged(msg: timer.mq.Request, modal: _Modal): timer.mq.Response { + if (msg.code === 'limitChanged') { + const data: timer.limit.Item[] = msg.data + const items = data.map(TimeLimitItem.of) + items?.length ? modal.process(items) : modal.hideModal() + return { code: 'success' } + } else { return { code: 'ignore' } } - if (!modal.isVisible()) { - return { code: 'ignore' } - } - modal.hideModal() - return { code: 'success' } } export default async function processLimit(url: string) { @@ -219,7 +221,7 @@ export default async function processLimit(url: string) { (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(handleLimitRemoved(msg, modal)) + (msg: timer.mq.Request, _sender, sendResponse: timer.mq.Callback) => sendResponse(handleLimitChanged(msg, modal)) ) } diff --git a/src/service/limit-service.ts b/src/service/limit-service.ts index 978d8507e..8002a00b6 100644 --- a/src/service/limit-service.ts +++ b/src/service/limit-service.ts @@ -36,35 +36,40 @@ async function select(cond?: QueryParam): Promise { .filter(item => url ? item.matches(url) : true) } -async function update({ cond, time, enabled, allowDelay }: timer.limit.Item, rewrite?: boolean): Promise { - if (rewrite === undefined) { - rewrite = true - } +/** + * Fired if the item is removed or disabled + * + * @param item + */ +async function handleLimitChanged() { + const allItems: TimeLimitItem[] = await select({ filterDisabled: false, url: undefined }) + chrome.tabs.query({}, tabs => 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) + }) + })) +} + +async function updateEnabled(item: timer.limit.Item): Promise { + const { cond, time, enabled, allowDelay } = item const limit: timer.limit.Rule = { cond, time, enabled, allowDelay } - await db.save(limit, rewrite) + await db.save(limit, true) + await handleLimitChanged() } async function updateDelay(item: timer.limit.Item) { await db.updateDelay(item.cond, item.allowDelay) + await handleLimitChanged() } async function remove(item: timer.limit.Item): Promise { await db.remove(item.cond) - const allItems: TimeLimitItem[] = await select({ filterDisabled: true, url: undefined }) - chrome.tabs.query({}, tabs => tabs.forEach(tab => { - if (allItems.find(item => item.matches(tab.url) && item.hasLimited())) { - // Needn't remove - return - } - chrome.tabs.sendMessage, timer.mq.Response>(tab.id, { - code: 'limitRemoved', - data: undefined - }, result => { - if (result?.code === "fail") { - console.error(`Failed to handle limit removed: cond=${JSON.stringify(item)}, msg=${result.msg}`) - } - }) - })) + await handleLimitChanged() } async function getLimited(url: string): Promise { @@ -116,7 +121,7 @@ async function moreMinutes(url: string, rules?: TimeLimitItem[]): Promise Date: Fri, 27 Jan 2023 12:51:36 +0800 Subject: [PATCH 094/168] Change point count --- script/user-chart/render.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index 3214a63f6..a3b98092f 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -14,6 +14,8 @@ import { EChartsType, init } from "echarts" const ALL_BROWSERS: Browser[] = ['firefox', 'edge', 'chrome'] +const POINT_COUNT = 200 + type OriginData = { [browser in Browser]: UserCount } @@ -64,7 +66,7 @@ function preProcess(originData: OriginData): ChartData { } // 3. zoom - const reduction = Math.floor(Object.keys(allDates).length / 150) + const reduction = Math.floor(Object.keys(allDates).length / POINT_COUNT) result.xAixs = zoom(result.xAixs, reduction) ALL_BROWSERS.forEach(b => result.yAixses[b] = zoom(result.yAixses[b], reduction)) return result From da177c55dad016138491471e33fea4a1b3991aa9 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 27 Jan 2023 13:07:43 +0800 Subject: [PATCH 095/168] Refactor --- script/user-chart/add.ts | 43 +++++++++++++++---- script/user-chart/argv.ts | 70 ------------------------------- script/user-chart/common.ts | 13 +++++- script/user-chart/index.ts | 20 --------- script/user-chart/render.ts | 31 +++++++------- script/user-chart/user-chart.d.ts | 6 +++ 6 files changed, 68 insertions(+), 115 deletions(-) delete mode 100644 script/user-chart/argv.ts delete mode 100644 script/user-chart/index.ts create mode 100644 script/user-chart/user-chart.d.ts diff --git a/script/user-chart/add.ts b/script/user-chart/add.ts index 6e2611538..bdac7699f 100644 --- a/script/user-chart/add.ts +++ b/script/user-chart/add.ts @@ -5,12 +5,36 @@ import { GistForm, updateGist as updateGistApi } from "@src/api/gist" -import { AddArgv, Argv, Browser } from "./argv" import fs from "fs" -import { descriptionOf, filenameOf, getExistGist } from "./common" +import { descriptionOf, filenameOf, getExistGist, validateTokenFromEnv } from "./common" import { exitWith } from "../util/process" -export type UserCount = Record +type AddArgv = { + browser: Browser + fileName: string +} + +function parseArgv(): AddArgv { + const argv = process.argv.slice(2) + const browserArgv = argv[0] + const fileName = argv[1] + if (!browserArgv || !fileName) { + exitWith("add.ts [c/e/f] [file_name]") + } + const browserArgvMap: Record = { + c: 'chrome', + e: 'edge', + f: 'firefox', + } + const browser: Browser = browserArgvMap[browserArgv] + if (!browser) { + exitWith("add.ts [c/e/f] [file_name]") + } + return { + browser, + fileName + } +} async function createGist(token: string, browser: Browser, data: UserCount) { const description = descriptionOf(browser) @@ -117,10 +141,11 @@ function rjust(str: string, num: number, padding: string): string { return Array.from(new Array(num - str.length).keys()).map(_ => padding).join('') + str } -export async function add(argv: Argv) { - const token = argv.gistToken - const browser = (argv as AddArgv).browser - const fileName = (argv as AddArgv).fileName +async function main() { + const token = validateTokenFromEnv() + const argv: AddArgv = parseArgv() + const browser = argv.browser + const fileName = argv.fileName const content = fs.readFileSync(fileName, { encoding: 'utf-8' }) let newData: UserCount = {} if (browser === 'chrome') { @@ -138,4 +163,6 @@ export async function add(argv: Argv) { } else { await updateGist(token, browser, newData, gist) } -} \ No newline at end of file +} + +main() \ No newline at end of file diff --git a/script/user-chart/argv.ts b/script/user-chart/argv.ts deleted file mode 100644 index 7fbaf8aee..000000000 --- a/script/user-chart/argv.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { exitWith } from "../util/process" - -type Cmd = - | 'add' - | 'render' - -type ArgvBase = { - cmd: Cmd - gistToken: string -} - -export type RenderArgv = ArgvBase & { - cmd: 'render' -} - -export type Browser = - | 'chrome' - | 'firefox' - | 'edge' - -export type AddArgv = ArgvBase & { - cmd: 'add' - browser: Browser - fileName: string -} - -export type Argv = - | RenderArgv - | AddArgv - -export function parseArgv(): Argv { - const argv = process.argv.slice(2) - const cmd: Cmd = argv[0] as Cmd - - const token = process.env.TIMER_USER_COUNT_GIST_TOKEN - if (!token) { - exitWith("Can't find token from env variable [TIMER_USER_COUNT_GIST_TOKEN]") - } - if (cmd === 'add') { - return parseAddArgv(argv, token) - } else if (cmd === 'render') { - return { gistToken: token, cmd: 'render' } as RenderArgv - } else { - console.error("Supported command: render, add") - process.exit() - } -} - -function parseAddArgv(argv: string[], token: string): AddArgv { - const browserArgv = argv[1] - const fileName = argv[2] - if (!browserArgv || !fileName) { - exitWith("add [c/e/f] [file_name]") - } - const browserArgvMap: Record = { - c: 'chrome', - e: 'edge', - f: 'firefox', - } - const browser: Browser = browserArgvMap[browserArgv] - if (!browser) { - exitWith("add [c/e/f] [file_name]") - } - return { - cmd: 'add', - gistToken: token, - browser, - fileName - } -} \ No newline at end of file diff --git a/script/user-chart/common.ts b/script/user-chart/common.ts index ca079da9f..9d8c46c7e 100644 --- a/script/user-chart/common.ts +++ b/script/user-chart/common.ts @@ -1,5 +1,16 @@ import { findTarget, Gist } from "@api/gist" -import { Browser } from "./argv" +import { exitWith } from "../util/process" + +/** + * Validate the token from environment variables + */ +export function validateTokenFromEnv(): string { + const token = process.env.TIMER_USER_COUNT_GIST_TOKEN + if (!token) { + exitWith("Can't find token from env variable [TIMER_USER_COUNT_GIST_TOKEN]") + } + return token +} /** * Calculate the gist description of target browser diff --git a/script/user-chart/index.ts b/script/user-chart/index.ts deleted file mode 100644 index 1cad7a01d..000000000 --- a/script/user-chart/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { add } from "./add" -import { parseArgv } from "./argv" -import { exitWith } from "../util/process" -import { render } from "./render" - -function main() { - const argv = parseArgv() - switch (argv.cmd) { - case 'add': - add(argv) - break - case 'render': - render(argv) - break - default: - exitWith('Unsupported cmd: ' + JSON.stringify(argv)) - } -} - -main() \ No newline at end of file diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index a3b98092f..7b8f3c446 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -7,9 +7,7 @@ import { GistForm, updateGist } from "@api/gist" -import { UserCount } from "./add" -import { Argv, Browser } from "./argv" -import { filenameOf, getExistGist } from "./common" +import { filenameOf, getExistGist, validateTokenFromEnv } from "./common" import { EChartsType, init } from "echarts" const ALL_BROWSERS: Browser[] = ['firefox', 'edge', 'chrome'] @@ -27,19 +25,6 @@ type ChartData = { } } -export async function render(argv: Argv): Promise { - const token = argv.gistToken - // 1. get all data - const originData: OriginData = await getOriginData(token) - // 2. pre-process data - const chartData = preProcess(originData) - // 3. render csv - const svg = render2Svg(chartData) - // 4. upload - await upload2Gist(token, svg) - process.exit() -} - function preProcess(originData: OriginData): ChartData { // 1. sort datess const dateSet = new Set() @@ -215,3 +200,17 @@ async function upload2Gist(token: string, svg: string) { console.log('Created new gist') } } + +async function main(): Promise { + const token = validateTokenFromEnv() + // 1. get all data + const originData: OriginData = await getOriginData(token) + // 2. pre-process data + const chartData = preProcess(originData) + // 3. render csv + const svg = render2Svg(chartData) + // 4. upload + await upload2Gist(token, svg) +} + +main() diff --git a/script/user-chart/user-chart.d.ts b/script/user-chart/user-chart.d.ts new file mode 100644 index 000000000..56d0300c8 --- /dev/null +++ b/script/user-chart/user-chart.d.ts @@ -0,0 +1,6 @@ +type Browser = + | 'chrome' + | 'firefox' + | 'edge' + +type UserCount = Record \ No newline at end of file From 8c7f4c449a1d8aa07480b09313245a8c63861209 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 2 Feb 2023 00:06:56 +0800 Subject: [PATCH 096/168] Support automatic data backup and optimize remote data display (#184) --- global.d.ts | 50 ++++- src/app/components/help-us/progress-list.ts | 2 +- .../option/components/appearance/index.ts | 2 +- .../option/components/backup/auto-input.ts | 61 ++++++ .../components/{backup.ts => backup/index.ts} | 30 ++- src/app/components/report/formatter.ts | 2 +- src/app/components/report/index.ts | 1 + .../report/table/columns/composition-table.ts | 74 +++++++ .../components/report/table/columns/focus.ts | 26 ++- .../components/report/table/columns/time.ts | 28 ++- src/app/components/report/table/index.ts | 37 ++-- src/background/alarm-manager.ts | 30 ++- src/background/backup-scheduler.ts | 48 +++++ src/background/index.ts | 7 +- .../backup/gist/compressor.ts | 0 .../backup/gist/coordinator.ts | 0 .../backup/gist/gist.d.ts | 0 .../backup/processor.ts | 10 +- .../backup/synchronizer.d.ts | 0 src/i18n/message/app/option.ts | 20 ++ src/i18n/message/app/report.ts | 30 +++ src/i18n/message/common/locale.ts | 69 ++++-- src/service/meta-service.ts | 22 ++ .../index.ts} | 200 ++++++++---------- src/service/timer-service/merge.ts | 103 +++++++++ src/util/constant/option.ts | 4 +- .../background/backup/gist/compressor.test.ts | 2 +- 27 files changed, 673 insertions(+), 185 deletions(-) create mode 100644 src/app/components/option/components/backup/auto-input.ts rename src/app/components/option/components/{backup.ts => backup/index.ts} (80%) create mode 100644 src/app/components/report/table/columns/composition-table.ts create mode 100644 src/background/backup-scheduler.ts rename src/{background => common}/backup/gist/compressor.ts (100%) rename src/{background => common}/backup/gist/coordinator.ts (100%) rename src/{background => common}/backup/gist/gist.d.ts (100%) rename src/{background => common}/backup/processor.ts (96%) rename src/{background => common}/backup/synchronizer.d.ts (100%) rename src/service/{timer-service.ts => timer-service/index.ts} (63%) create mode 100644 src/service/timer-service/merge.ts diff --git a/global.d.ts b/global.d.ts index c82303aa6..514223c0e 100644 --- a/global.d.ts +++ b/global.d.ts @@ -143,6 +143,14 @@ declare namespace timer { * 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 @@ -165,6 +173,12 @@ declare namespace timer { * @since 1.2.0 */ cid?: string + backup?: { + [key in timer.backup.Type]?: { + ts: number + msg?: string + } + } } } @@ -249,6 +263,10 @@ declare namespace timer { * @since 0.1.5 */ mergedHosts: Row[] + /** + * The composition of data when querying remote + */ + composition?: RemoteComposition /** * Icon url * @@ -259,15 +277,35 @@ declare namespace timer { * The alias name of this Site, always is the title of its homepage by detected */ alias?: string - } - /** - * @since 1.2.0 - */ - type RemoteRow = RowBase & { + /** + * The id of client where the remote data is storaged + */ + cid?: string /** * The name of client where the remote data is storaged */ - clientName?: string + 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[] } } diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index 0e820c9b7..53a581343 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -62,7 +62,7 @@ function renderProgressItem(progressInfo: ProgressInfo): VNode { const { locale, progress } = progressInfo return h(ElProgress, { percentage: progress, strokeWidth: 22, status: computeType(progress) }, () => [ h('span', { class: 'progress-text' }, `${progress}%`), - h('span', { class: 'language-name' }, localeMessages[locale] || locale), + h('span', { class: 'language-name' }, localeMessages[locale]?.name || locale), ]) } diff --git a/src/app/components/option/components/appearance/index.ts b/src/app/components/option/components/appearance/index.ts index 8c2dc4e55..b97b9d672 100644 --- a/src/app/components/option/components/appearance/index.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -75,7 +75,7 @@ const locale = (option: UnwrapRef) => h(ElSelect, locale => h(ElOption, { value: locale, label: locale === "default" ? t(msg => msg.option.appearance.locale.default) - : localeMessages[locale] + : localeMessages[locale].name }) ) }) diff --git a/src/app/components/option/components/backup/auto-input.ts b/src/app/components/option/components/backup/auto-input.ts new file mode 100644 index 000000000..8a92177ca --- /dev/null +++ b/src/app/components/option/components/backup/auto-input.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t, tN } from "@app/locale" +import { ElInputNumber, ElSwitch } from "element-plus" +import { defineComponent, ref, watch, h } from "vue" +import localeMessages from "@i18n/message/common/locale" +import { locale } from "@i18n" + +const _default = defineComponent({ + name: 'BackUpAutoInput', + props: { + autoBackup: Boolean, + interval: Number + }, + emits: { + change: (_autoBackUp: boolean, _interval: number) => true + }, + setup(props, ctx) { + const autoBackUp = ref(props.autoBackup) + const interval = ref(props.interval) + watch(() => props.autoBackup, newVal => autoBackUp.value = newVal) + watch(() => props.interval, newVal => interval.value = newVal) + + const handleChange = () => ctx.emit('change', autoBackUp.value, interval.value) + + return () => { + const result = [ + h(ElSwitch, { + modelValue: autoBackUp.value, + onChange: (newVal: boolean) => { + autoBackUp.value = newVal + handleChange() + } + }), + ' ' + t(msg => msg.option.backup.auto.label), + ] + autoBackUp.value && result.push( + localeMessages[locale].comma || ' ', + ...tN(msg => msg.option.backup.auto.interval, { + input: h(ElInputNumber, { + min: 10, + size: 'small', + modelValue: interval.value, + onChange(newVal) { + interval.value = newVal + handleChange() + }, + }) + }) + ) + return result + } + }, +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/components/backup.ts b/src/app/components/option/components/backup/index.ts similarity index 80% rename from src/app/components/option/components/backup.ts rename to src/app/components/option/components/backup/index.ts index cc585a84d..0cbd73441 100644 --- a/src/app/components/option/components/backup.ts +++ b/src/app/components/option/components/backup/index.ts @@ -9,12 +9,13 @@ import { Ref } from "vue" import { t } from "@app/locale" import optionService from "@service/option-service" -import processor from "@src/background/backup/processor" +import processor from "@src/common/backup/processor" import { defaultBackup } from "@util/constant/option" import { ElInput, ElOption, ElSelect, ElDivider, ElAlert, ElButton, ElMessage, ElLoading } from "element-plus" import { defineComponent, ref, h } from "vue" -import { renderOptionItem, tooltip } from "../common" +import { renderOptionItem, tooltip } from "../../common" import { UploadFilled } from "@element-plus/icons-vue" +import BackUpAutoInput from "./auto-input" const ALL_TYPES: timer.backup.Type[] = [ 'none', @@ -74,6 +75,8 @@ const _default = defineComponent({ const type: Ref = ref(DEFAULT.backupType) const auth: Ref = ref('') const clientName: Ref = ref(DEFAULT.clientName) + const autoBackUp: Ref = ref(DEFAULT.autoBackUp) + const autoBackUpInterval: Ref = ref(DEFAULT.autoBackUpInterval) optionService.getAllOption().then(currentVal => { clientName.value = currentVal.clientName @@ -81,6 +84,8 @@ const _default = defineComponent({ if (type.value) { auth.value = currentVal.backupAuths?.[type.value] } + autoBackUp.value = currentVal.autoBackUp + autoBackUpInterval.value = currentVal.autoBackUpInterval }) function handleChange() { @@ -89,7 +94,9 @@ const _default = defineComponent({ const newOption: timer.option.BackupOption = { backupType: type.value, backupAuths, - clientName: clientName.value || DEFAULT.clientName + clientName: clientName.value || DEFAULT.clientName, + autoBackUp: autoBackUp.value, + autoBackUpInterval: autoBackUpInterval.value, } optionService.setBackupOption(newOption) } @@ -122,8 +129,9 @@ const _default = defineComponent({ ctx.expose({ async reset() { - // Only reset type + // Only reset type and auto flag type.value = DEFAULT.backupType + autoBackUp.value = DEFAULT.autoBackUp handleChange() } }) @@ -144,6 +152,18 @@ const _default = defineComponent({ ) ] type.value !== 'none' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: h(BackUpAutoInput, { + autoBackup: autoBackUp.value, + interval: autoBackUpInterval.value, + onChange(newAutoBackUp, newInterval) { + autoBackUp.value = newAutoBackUp + autoBackUpInterval.value = newInterval + handleChange() + } + }) + }, _msg => '{input}', t(msg => msg.option.no)), h(ElDivider), renderOptionItem({ input: authInput(auth, handleChange, handleTest), @@ -162,7 +182,7 @@ const _default = defineComponent({ type: 'primary', icon: UploadFilled, onClick: handleBackup - }, () => t(msg => msg.option.backup.operation)) + }, () => t(msg => msg.option.backup.operation)), ) return h('div', nodes) } diff --git a/src/app/components/report/formatter.ts b/src/app/components/report/formatter.ts index 85223b6ec..c45cb48f2 100644 --- a/src/app/components/report/formatter.ts +++ b/src/app/components/report/formatter.ts @@ -27,7 +27,7 @@ export function dateFormatter(date: string): string { * @param timeFormat * @param hideUnit */ -export const periodFormatter = (milliseconds: number, timeFormat?: timer.app.TimeFormat, hideUnit?: boolean) => { +export function periodFormatter(milliseconds: number, timeFormat?: timer.app.TimeFormat, hideUnit?: boolean): string { const format = timeFormat || "default" if (milliseconds === undefined) { if (format === 'default') { diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index 61a7906ff..308b4eb55 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -260,6 +260,7 @@ const _default = defineComponent({ mergeHost: filterOption.mergeHost, timeFormat: filterOption.timeFormat, dateRange: filterOption.dateRange, + readRemote: remoteRead.value, data: data.value, defaultSort: sort, ref: tableEl, diff --git a/src/app/components/report/table/columns/composition-table.ts b/src/app/components/report/table/columns/composition-table.ts new file mode 100644 index 000000000..db137092e --- /dev/null +++ b/src/app/components/report/table/columns/composition-table.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { computed, ComputedRef, defineComponent, h, PropType } from "vue" +import { ElTable, ElTableColumn } from "element-plus" +import { sum } from "@util/array" +import { t } from "@app/locale" + +type Row = { + name: string + value: number + percent?: string +} + +type ValueFormatter = (val: number) => string + +const CLIENT_NAME = t(msg => msg.report.remoteReading.table.client) +const VALUE = t(msg => msg.report.remoteReading.table.value) +const LOCAL_DATA = t(msg => msg.report.remoteReading.table.localData) +const PERCENTAGE = t(msg => msg.report.remoteReading.table.percentage) + +function computeRows(data: timer.stat.RemoteCompositionVal[]): Row[] { + const rows: Row[] = data.map(e => typeof e === 'number' + ? { name: LOCAL_DATA, value: e || 0 } + : { name: e.cname || e.cid, value: e.value } + ) + const total = sum(rows.map(row => row.value)) + total && rows.forEach(row => row.percent = (row.value / total * 100).toFixed(2) + ' %') + rows.sort((a, b) => b.value - a.value) + return rows +} + +const _default = defineComponent({ + name: 'CompositionChart', + props: { + data: Array as PropType, + valueFormatter: Function as PropType, + }, + setup(props) { + const rows: ComputedRef = computed(() => computeRows(props.data)) + return () => h('div', { style: { width: '400px' } }, + h(ElTable, { + data: rows.value, + size: 'small', + border: true, + }, () => [ + h(ElTableColumn, { + label: CLIENT_NAME, + formatter: (row: Row) => row.name, + align: 'center', + width: 150, + }), + h(ElTableColumn, { + label: VALUE, + formatter: (row: Row) => props.valueFormatter(row.value), + align: 'center', + width: 150, + }), + h(ElTableColumn, { + label: PERCENTAGE, + align: 'center', + formatter: (row: Row) => row.percent, + width: 100, + }), + ]) + ) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/report/table/columns/focus.ts b/src/app/components/report/table/columns/focus.ts index 1bd81334c..4389c637f 100644 --- a/src/app/components/report/table/columns/focus.ts +++ b/src/app/components/report/table/columns/focus.ts @@ -8,18 +8,18 @@ import type { PropType } from "vue" import { t } from "@app/locale" -import { ElTableColumn } from "element-plus" +import { Effect, ElTableColumn, ElTooltip } from "element-plus" import { defineComponent, h } from "vue" import { periodFormatter } from "../../formatter" +import CompositionTable from './composition-table' const columnLabel = t(msg => msg.item.focus) const _default = defineComponent({ name: "FocusColumn", props: { - timeFormat: { - type: String as PropType - } + timeFormat: String as PropType, + readRemote: Boolean, }, setup(props) { return () => h(ElTableColumn, { @@ -29,7 +29,23 @@ const _default = defineComponent({ align: "center", sortable: "custom" }, { - default: ({ row }: { row: timer.stat.Row }) => periodFormatter(row.focus, props.timeFormat || "default") + default({ row }: { row: timer.stat.Row }) { + const valueStr = periodFormatter(row.focus, props.timeFormat || "default") + if (!props.readRemote) { + return valueStr + } + return h(ElTooltip, { + placement: "top", + effect: Effect.LIGHT, + offset: 10 + }, { + default: () => valueStr, + content: () => h(CompositionTable, { + data: row.composition?.focus || [], + valueFormatter: val => periodFormatter(val, props.timeFormat || 'default'), + }) + }) + } }) } }) diff --git a/src/app/components/report/table/columns/time.ts b/src/app/components/report/table/columns/time.ts index b9f3c2692..6c93af23b 100644 --- a/src/app/components/report/table/columns/time.ts +++ b/src/app/components/report/table/columns/time.ts @@ -6,14 +6,18 @@ */ import { defineComponent, h } from "vue" -import { ElTableColumn } from "element-plus" +import { Effect, ElTableColumn, ElTooltip } from "element-plus" import { t } from "@app/locale" +import CompositionTable from "./composition-table" const columnLabel = t(msg => msg.item.time) const _default = defineComponent({ name: "TimeColumn", - setup() { + props: { + readRemote: Boolean, + }, + setup(props) { return () => h(ElTableColumn, { prop: "time", label: columnLabel, @@ -21,9 +25,25 @@ const _default = defineComponent({ align: 'center', sortable: 'custom' }, { - default: ({ row }: { row: timer.stat.Row }) => row.time?.toString?.() || '0' + default: ({ row }: { row: timer.stat.Row }) => { + const valueStr = row.time?.toString?.() || '0' + if (!props.readRemote) { + return valueStr + } + return h(ElTooltip, { + placement: "top", + effect: Effect.LIGHT, + offset: 10 + }, { + default: () => valueStr, + content: () => h(CompositionTable, { + data: row.composition?.time || [], + valueFormatter: val => val?.toString() || '0' + }) + }) + } }) } }) -export default _default \ No newline at end of file +export default _default diff --git a/src/app/components/report/table/index.ts b/src/app/components/report/table/index.ts index dcaaaaa14..17aee7099 100644 --- a/src/app/components/report/table/index.ts +++ b/src/app/components/report/table/index.ts @@ -8,7 +8,7 @@ import type { PropType } from "vue" import { ElTable } from "element-plus" -import { defineComponent, h } from "vue" +import { defineComponent, h, ref, watch } from "vue" import SelectionColumn from "./columns/selection" import DateColumn from "./columns/date" import HostColumn from "./columns/host" @@ -26,7 +26,8 @@ const _default = defineComponent({ mergeHost: Boolean, timeFormat: String as PropType, dateRange: Array as PropType, - whitelist: Array as PropType + whitelist: Array as PropType, + readRemote: Boolean, }, emits: { sortChange: (_newSortInfo: SortInfo) => true, @@ -36,6 +37,8 @@ const _default = defineComponent({ }, setup(props, ctx) { let selectedRows: timer.stat.Row[] = [] + const readRemote = ref(props.readRemote) + watch(() => props.readRemote, newVal => readRemote.value = newVal) ctx.expose({ getSelected(): timer.stat.Row[] { return selectedRows || [] @@ -56,20 +59,22 @@ const _default = defineComponent({ h(SelectionColumn, { disabled: props.mergeHost }) ] props.mergeDate || result.push(h(DateColumn)) - result.push(h(HostColumn, { mergeHost: props.mergeHost })) - result.push(h(AliasColumn, { - onAliasChange: (host: string, newAlias: string) => ctx.emit("aliasChange", host, newAlias) - })) - result.push(h(FocusColumn, { timeFormat: props.timeFormat })) - result.push(h(TimeColumn)) - result.push(h(OperationColumn, { - mergeDate: props.mergeDate, - mergeHost: props.mergeHost, - dateRange: props.dateRange, - whitelist: props.whitelist, - onDelete: (row: timer.stat.Row) => ctx.emit("itemDelete", row), - onWhitelistChange: (host: string, addOrRemove: boolean) => ctx.emit("whitelistChange", host, addOrRemove) - })) + result.push( + h(HostColumn, { mergeHost: props.mergeHost }), + h(AliasColumn, { + onAliasChange: (host: string, newAlias: string) => ctx.emit("aliasChange", host, newAlias) + }), + h(FocusColumn, { timeFormat: props.timeFormat, readRemote: props.readRemote }), + h(TimeColumn, { readRemote: props.readRemote }), + h(OperationColumn, { + mergeDate: props.mergeDate, + mergeHost: props.mergeHost, + dateRange: props.dateRange, + whitelist: props.whitelist, + onDelete: (row: timer.stat.Row) => ctx.emit("itemDelete", row), + onWhitelistChange: (host: string, addOrRemove: boolean) => ctx.emit("whitelistChange", host, addOrRemove) + }), + ) return result }) } diff --git a/src/background/alarm-manager.ts b/src/background/alarm-manager.ts index 0a6db16d5..4b1845cef 100644 --- a/src/background/alarm-manager.ts +++ b/src/background/alarm-manager.ts @@ -8,6 +8,9 @@ type _Handler = (alarm: chrome.alarms.Alarm) => void const ALARM_PREFIX = 'timer-alarm-' + chrome.runtime.id + '-' const ALARM_PREFIX_LENGTH = ALARM_PREFIX.length +const getInnerName = (outerName: string) => ALARM_PREFIX + outerName +const getOuterName = (innerName: string) => innerName.substring(ALARM_PREFIX_LENGTH) + /** * The manager of alarms * @@ -27,10 +30,10 @@ class AlarmManager { // Unknown alarm return } - const realName = name.substring(ALARM_PREFIX_LENGTH) - const config: _AlarmConfig = this.alarms[realName] + const innerName = getOuterName(name) + const config: _AlarmConfig = this.alarms[innerName] if (!config) { - // Not register, or unregistered + // Not registered, or removed return } // Handle alarm event @@ -42,20 +45,31 @@ class AlarmManager { }) } - setInterval(name: string, interval: number, handler: _Handler): void { + /** + * Set a alarm to do sth with interval + */ + setInterval(outerName: string, interval: number, handler: _Handler): void { if (!interval || !handler) { return } const config: _AlarmConfig = { handler, interval } - if (this.alarms[name]) { + if (this.alarms[outerName]) { // Existed, only update the config - this.alarms[name] = config + this.alarms[outerName] = config return } // Initialize config - this.alarms[name] = config + this.alarms[outerName] = config // Create new one alarm - chrome.alarms.create(ALARM_PREFIX + name, { when: Date.now() + interval }) + chrome.alarms.create(getInnerName(outerName), { when: Date.now() + interval }) + } + + /** + * Remove a interval + */ + remove(outerName: string) { + delete this.alarms[outerName] + chrome.alarms.clear(getInnerName(outerName)) } } diff --git a/src/background/backup-scheduler.ts b/src/background/backup-scheduler.ts new file mode 100644 index 000000000..9f6f837ae --- /dev/null +++ b/src/background/backup-scheduler.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import optionService from "@service/option-service" +import alarmManager from "./alarm-manager" +import processor from "@src/common/backup/processor" + +const ALARM_NAME = 'auto-backup-data' + +class BackupScheduler { + needBackup = false + /** + * Interval of millseconds + */ + interval: number = 0 + + init() { + optionService.getAllOption().then(opt => this.handleOption(opt)) + optionService.addOptionChangeListener(opt => this.handleOption(opt)) + } + + private handleOption(option: timer.option.BackupOption) { + this.needBackup = !!option.autoBackUp + this.interval = (option.autoBackUpInterval || 0) * 60 * 1000 + if (this.needSchedule()) { + alarmManager.setInterval(ALARM_NAME, this.interval, () => this.doBackup()) + } else { + alarmManager.remove(ALARM_NAME) + } + } + + private needSchedule(): boolean { + return !!this.needBackup && !!this.interval + } + + private async doBackup(): Promise { + const result = await processor.syncData() + if (!result.success) { + console.warn(`Failed to backup ts=${Date.now()}, msg=${result.errorMsg}`) + } + } +} + +export default BackupScheduler \ No newline at end of file diff --git a/src/background/index.ts b/src/background/index.ts index cad22a406..6abeb382d 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -20,6 +20,7 @@ import MessageDispatcher from "./message-dispatcher" import initLimitProcesser from "./limit-processor" import initCsHandler from "./content-script-handler" import { isBrowserUrl } from "@util/pattern" +import BackupScheduler from "./backup-scheduler" // Open the log of console openLog() @@ -41,7 +42,10 @@ new IconAndAliasCollector().listen() // Process version new VersionManager().init() -// Mange the context menus +// Backup scheduler +new BackupScheduler().init() + +// Manage the context menus WhitelistMenuManager() // Browser action menu @@ -76,4 +80,5 @@ chrome.runtime.onInstalled.addListener(async detail => { new UninstallListener().listen() }) +// Start message dispatcher messageDispatcher.start() \ No newline at end of file diff --git a/src/background/backup/gist/compressor.ts b/src/common/backup/gist/compressor.ts similarity index 100% rename from src/background/backup/gist/compressor.ts rename to src/common/backup/gist/compressor.ts diff --git a/src/background/backup/gist/coordinator.ts b/src/common/backup/gist/coordinator.ts similarity index 100% rename from src/background/backup/gist/coordinator.ts rename to src/common/backup/gist/coordinator.ts diff --git a/src/background/backup/gist/gist.d.ts b/src/common/backup/gist/gist.d.ts similarity index 100% rename from src/background/backup/gist/gist.d.ts rename to src/common/backup/gist/gist.d.ts diff --git a/src/background/backup/processor.ts b/src/common/backup/processor.ts similarity index 96% rename from src/background/backup/processor.ts rename to src/common/backup/processor.ts index f3a37baad..e3111c4f2 100644 --- a/src/background/backup/processor.ts +++ b/src/common/backup/processor.ts @@ -171,10 +171,12 @@ class Processor { const clients: Client[] = (await coordinator.listAllClients(context)).filter(a => a.id !== cid) || [] clients.push(client) await coordinator.updateClients(context, clients) + // Update time + metaService.updateBackUpTime(type, Date.now()) return success() } - async query(type: timer.backup.Type, auth: string, start: Date, end: Date): Promise { + async query(type: timer.backup.Type, auth: string, start: Date, end: Date): Promise { const coordinator: Coordinator = this.coordinators?.[type] if (!coordinator) return [] let cid = await metaService.getCid() @@ -186,7 +188,7 @@ class Processor { const allClients = (await coordinator.listAllClients(context)) .filter(c => filterClient(c, cid, startStr, endStr)) // 3. iterate month and clients - const result: timer.stat.RemoteRow[] = [] + const result: timer.stat.Row[] = [] const allYearMonth = new MonthIterator(start, end || new Date()).toArray() await Promise.all(allClients.map(async client => { const { id, name } = client @@ -195,7 +197,9 @@ class Processor { .filter(row => filterDate(row, startStr, endStr)) .forEach(row => result.push({ ...row, - clientName: name + cid: id, + cname: name, + mergedHosts: [] })) })) })) diff --git a/src/background/backup/synchronizer.d.ts b/src/common/backup/synchronizer.d.ts similarity index 100% rename from src/background/backup/synchronizer.d.ts rename to src/common/backup/synchronizer.d.ts diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 54be89966..9035cef47 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -73,6 +73,10 @@ export type OptionMessage = { alert: string test: string operation: string + auto: { + label: string + interval: string + } } resetButton: string resetSuccess: string @@ -164,6 +168,10 @@ const _default: Messages = { alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', test: '测试', operation: '备份数据', + auto: { + label: '是否开启自动备份', + interval: '每 {input} 分钟备份一次', + } }, resetButton: '恢复默认', resetSuccess: '成功重置为默认值', @@ -245,6 +253,10 @@ const _default: Messages = { alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', test: '測試', operation: '備份數據', + auto: { + label: '是否開啟自動備份', + interval: '每 {input} 分鐘備份一次', + } }, resetButton: '恢複默認', resetSuccess: '成功重置爲默認值', @@ -326,6 +338,10 @@ const _default: Messages = { alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', test: 'Test', operation: 'Backup', + auto: { + label: 'Whether to enable automatic backup', + interval: 'and run every {input} minutes', + }, }, resetButton: 'Reset', resetSuccess: 'Reset to default successfully!', @@ -407,6 +423,10 @@ const _default: Messages = { alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', test: 'テスト', operation: 'バックアップ', + auto: { + label: '自動バックアップを有効にするかどうか', + interval: ' {input} 分ごとに実行', + }, }, resetButton: 'リセット', resetSuccess: 'デフォルトに正常にリセット', diff --git a/src/i18n/message/app/report.ts b/src/i18n/message/app/report.ts index 061f9ad01..dccc20432 100644 --- a/src/i18n/message/app/report.ts +++ b/src/i18n/message/app/report.ts @@ -29,6 +29,12 @@ export type ReportMessage = { remoteReading: { on: string off: string + table: { + client: string + localData: string + value: string + percentage: string + } } } @@ -57,6 +63,12 @@ const _default: Messages = { remoteReading: { on: '正在查询远端备份数据', off: '单击以开启远端备份数据查询功能', + table: { + client: '客户端', + localData: '本地数据', + value: '对应数值', + percentage: '百分比', + } }, }, zh_TW: { @@ -83,6 +95,12 @@ const _default: Messages = { remoteReading: { on: '正在查詢遠端備份數據', off: '單擊以開啟遠端備份數據查詢功能', + table: { + client: '客户端', + localData: '本地數據', + value: '對應數值', + percentage: '百分比', + } }, }, en: { @@ -109,6 +127,12 @@ const _default: Messages = { remoteReading: { on: 'Reading remote backuped data', off: 'Click to read remote backuped data', + table: { + client: 'Client\'s Name', + localData: 'Local Data', + value: 'Value', + percentage: 'Percentage', + } }, }, ja: { @@ -135,6 +159,12 @@ const _default: Messages = { remoteReading: { on: 'リモート バックアップ データのクエリ', off: 'クリックして、リモート バックアップ データのクエリ機能を有効にします', + table: { + client: 'クライアントの名前', + localData: 'ローカル データ', + value: '対応する値', + percentage: 'パーセンテージ', + }, }, }, } diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index b48008dc7..854cc1cca 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -11,25 +11,62 @@ * @since 0.8.0 */ export type LocaleMessages = { - [locale in timer.Locale | timer.TranslatingLocale]: string + [locale in timer.Locale | timer.TranslatingLocale]: { + name: string + comma?: string + } } const _default: LocaleMessages = { - zh_CN: '简体中文', - zh_TW: '正體中文', - en: 'English', - ja: '日本語', - pl: 'Polski', - pt: 'Português', - pt_BR: 'Portugues, Brasil', - ko: '한국인', - de: 'Deutsch', - es: 'Español', - ru: 'Русский', - uk: "українська", - fr: "Français", - it: "italiano", - sv: "Sverige", + zh_CN: { + name: '简体中文', + comma: ',' + }, + zh_TW: { + name: '正體中文', + comma: ',', + }, + en: { + name: 'English', + comma: ', ' + }, + ja: { + name: '日本語', + comma: '、' + }, + pl: { + name: 'Polski' + }, + pt: { + name: 'Português' + }, + pt_BR: { + name: 'Portugues, Brasil' + }, + ko: { + name: '한국인' + }, + de: { + name: 'Deutsch' + }, + es: { + name: 'Español' + }, + ru: { + name: 'Русский' + }, + uk: { + name: "українська" + }, + fr: { + name: "Français" + }, + it: { + name: "italiano" + }, + sv: { + name: "Sverige" + }, } export default _default \ No newline at end of file diff --git a/src/service/meta-service.ts b/src/service/meta-service.ts index 9a617a3d7..aeb6593a4 100644 --- a/src/service/meta-service.ts +++ b/src/service/meta-service.ts @@ -57,6 +57,20 @@ async function updateCid(newCid: string) { await db.update(meta) } +async function updateBackUpTime(type: timer.backup.Type, time: number) { + const meta = await db.getMeta() + if (!meta.backup) { + meta.backup = {} + } + meta.backup[type] = { ts: time } + await db.update(meta) +} + +async function getLastBackUp(type: timer.backup.Type): Promise<{ ts: number, msg?: string }> { + const meta = await db.getMeta() + return meta?.backup?.[type] +} + class MetaService { getInstallTime = getInstallTime updateInstallTime = updateInstallTime @@ -70,6 +84,14 @@ class MetaService { updateCid = updateCid increaseApp = increaseApp increasePopup = increasePopup + /** + * @since 1.4.7 + */ + updateBackUpTime = updateBackUpTime + /** + * @since 1.4.7 + */ + getLastBackUp = getLastBackUp } export default new MetaService() \ No newline at end of file diff --git a/src/service/timer-service.ts b/src/service/timer-service/index.ts similarity index 63% rename from src/service/timer-service.ts rename to src/service/timer-service/index.ts index 0951c2815..abc010168 100644 --- a/src/service/timer-service.ts +++ b/src/service/timer-service/index.ts @@ -6,17 +6,18 @@ */ import TimerDatabase, { TimerCondition } from "@db/timer-database" -import { log } from "../common/logger" -import CustomizedHostMergeRuler from "./components/host-merge-ruler" +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, rowOf } from "@util/stat" +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/background/backup/processor" +import processor from "@src/common/backup/processor" import { getBirthday } from "@util/time" +import { mergeDate, mergeHost } from "./merge" const storage = chrome.storage.local @@ -78,6 +79,79 @@ 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 @@ -178,17 +252,17 @@ class TimerService { param = param || {} let origin = await timerDatabase.select(param as TimerCondition) if (param.inclusiveRemote) { - origin = await this.processRemote(param, origin) + origin = await processRemote(param, origin) } // Process after select // 1st merge if (param.mergeHost) { // Merge with rules - origin = await this.mergeHost(origin) + 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 = this.mergeDate(origin)) + param.mergeDate && (origin = mergeDate(origin)) // 2nd sort this.processSort(origin, param) // 3rd get icon url and alias if need @@ -205,59 +279,6 @@ class TimerService { return timerDatabase.get(host, date) } - private async processRemote( - param: TimerCondition, - origin: timer.stat.Row[] - ): Promise { - const { backupType, backupAuths } = await optionDatabase.getOption() - const auth = backupAuths?.[backupType] - const canReadRemote = await this.canReadRemote0(backupType, auth) - if (!canReadRemote) { - return origin - } - // Map to merge - const originMap: Record = {} - origin.forEach(row => originMap[this.keyOf(row)] = row) - // 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 => { - const key = this.keyOf(row) - const exist = originMap[key] - if (exist) { - exist.focus += row.focus || 0 - exist.time += row.time || 0 - } else { - originMap[key] = { ...row, mergedHosts: [] } - } - }) - return Object.values(originMap) - } - - private keyOf(row: timer.stat.RowKey) { - return `${row.date}${row.host}` - } - async selectByPage( param?: TimerQueryParam, page?: timer.common.PageQuery, @@ -287,55 +308,6 @@ class TimerService { return paramHost ? origin.filter(o => o.host.includes(paramHost)) : origin } - private async mergeHost(origin: T[]): Promise { - const newRows = [] - const map = {} - - // Generate ruler - const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() - const mergeRuler = new CustomizedHostMergeRuler(mergeRuleItems) - - origin.forEach(o => { - const host = o.host - const date = o.date - let mergedHost = mergeRuler.merge(host) - const merged = this.merge(map, o, mergedHost + date) - merged.host = mergedHost - const mergedHosts = merged.mergedHosts || (merged.mergedHosts = []) - mergedHosts.push(o) - }) - for (let key in map) { - newRows.push(map[key]) - } - return newRows - } - - private mergeDate(origin: T[]): T[] { - const newRows = [] - const map = {} - - origin.forEach(o => this.merge(map, o, o.host).date = '') - for (let key in map) { - newRows.push(map[key]) - } - return newRows - } - - private merge(map: {}, origin: timer.stat.Row, key: string): timer.stat.Row { - let exist: timer.stat.Row = map[key] - if (exist === undefined) { - exist = map[key] = rowOf({ host: origin.host, date: origin.date }) - exist.mergedHosts = origin.mergedHosts || [] - } - exist.time += origin.time - exist.focus += origin.focus - - origin.mergedHosts && origin.mergedHosts.forEach(originHost => - !exist.mergedHosts.find(existOrigin => existOrigin.host === originHost.host) && exist.mergedHosts.push(originHost) - ) - return exist - } - /** * Aable to read remote backup data * @@ -344,11 +316,7 @@ class TimerService { */ async canReadRemote(): Promise { const { backupType, backupAuths } = await optionDatabase.getOption() - return await this.canReadRemote0(backupType, backupAuths?.[backupType]) - } - - private async canReadRemote0(backupType: timer.backup.Type, auth: string): Promise { - return backupType && backupType !== 'none' && !await processor.test(backupType, auth) + return await canReadRemote0(backupType, backupAuths?.[backupType]) } } diff --git a/src/service/timer-service/merge.ts b/src/service/timer-service/merge.ts new file mode 100644 index 000000000..3d4cfb392 --- /dev/null +++ b/src/service/timer-service/merge.ts @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import MergeRuleDatabase from "@db/merge-rule-database" +import CustomizedHostMergeRuler from "@service/components/host-merge-ruler" + +const storage = chrome.storage.local + +const mergeRuleDatabase = new MergeRuleDatabase(storage) + +function merge(map: Record, origin: timer.stat.Row, key: string): timer.stat.Row { + let exist: timer.stat.Row = map[key] + !exist && (exist = map[key] = { + host: origin.host, + date: origin.date, + focus: 0, + time: 0, + mergedHosts: [], + composition: { focus: [], time: [] }, + cid: origin.cid, + cname: origin.cname, + }) + + exist.time += origin.time + exist.focus += origin.focus + exist.composition = mergeComposition(exist.composition, origin.composition) + + origin.mergedHosts && origin.mergedHosts.forEach(originHost => + !exist.mergedHosts.find(existOrigin => existOrigin.host === originHost.host) && exist.mergedHosts.push(originHost) + ) + return exist +} + +type _RemoteCompositionMap = Record<'_' | string, timer.stat.RemoteCompositionVal> + +function mergeComposition(c1: timer.stat.RemoteComposition, c2: timer.stat.RemoteComposition): timer.stat.RemoteComposition { + const focusMap: _RemoteCompositionMap = {} + const timeMap: _RemoteCompositionMap = {} + c1?.focus?.forEach(e => accCompositionValue(focusMap, e)) + c2?.focus?.forEach(e => accCompositionValue(focusMap, e)) + c1?.time?.forEach(e => accCompositionValue(timeMap, e)) + c2?.time?.forEach(e => accCompositionValue(timeMap, e)) + + const result = { + focus: Object.values(focusMap), + time: Object.values(timeMap), + } + return result +} + +function accCompositionValue(map: _RemoteCompositionMap, value: timer.stat.RemoteCompositionVal) { + if (typeof value === 'number') { + const cid = '_' + const existVal = map[cid] + if (!existVal || typeof existVal !== 'number') { + map[cid] = value + } else { + map[cid] = existVal + value + } + } else { + const cid = value.cid + const existVal = map[cid] + if (!existVal || typeof existVal === 'number') { + map[cid] = value + } else { + existVal.value = existVal.value + value.value + } + } +} + +export function mergeDate(origin: T[]): T[] { + const map: Record = {} + origin.forEach(o => merge(map, o, o.host).date = '') + const newRows = Object.values(map) + return newRows +} + +export async function mergeHost(origin: T[]): Promise { + const newRows = [] + const map = {} + + // Generate ruler + const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() + const mergeRuler = new CustomizedHostMergeRuler(mergeRuleItems) + + origin.forEach(o => { + const host = o.host + const date = o.date + let mergedHost = mergeRuler.merge(host) + const merged = merge(map, o, mergedHost + date) + merged.host = mergedHost + const mergedHosts = merged.mergedHosts || (merged.mergedHosts = []) + mergedHosts.push(o) + }) + for (let key in map) { + newRows.push(map[key]) + } + return newRows +} \ No newline at end of file diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index ce5a73373..ba4d7b178 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -50,7 +50,9 @@ export function defaultBackup(): timer.option.BackupOption { return { backupType: 'none', clientName: 'unknown', - backupAuths: {} + backupAuths: {}, + autoBackUp: false, + autoBackUpInterval: 30, } } diff --git a/test/background/backup/gist/compressor.test.ts b/test/background/backup/gist/compressor.test.ts index c06d4670b..642fc5aff 100644 --- a/test/background/backup/gist/compressor.test.ts +++ b/test/background/backup/gist/compressor.test.ts @@ -1,4 +1,4 @@ -import { devide2Buckets, gistData2Rows } from "@src/background/backup/gist/compressor" +import { devide2Buckets, gistData2Rows } from "@src/common/backup/gist/compressor" test('devide 1', () => { const rows: timer.stat.Row[] = [{ From 5d3b14373039acb171158e4c712bc5a3f800b752 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 3 Feb 2023 23:25:49 +0800 Subject: [PATCH 097/168] v1.4.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4de4d507..ebf00a1f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.6", + "version": "1.4.7", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From b4599112ae8a3e2e3392a995d62345ada44b9193 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 8 Feb 2023 21:54:07 +0800 Subject: [PATCH 098/168] Fix timer error (#188) --- src/background/alarm-manager.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/background/alarm-manager.ts b/src/background/alarm-manager.ts index 4b1845cef..2abb14e56 100644 --- a/src/background/alarm-manager.ts +++ b/src/background/alarm-manager.ts @@ -38,10 +38,12 @@ class AlarmManager { } // Handle alarm event config.handler?.(alarm) + const nextTs = Date.now() + config.interval // Clear this one - chrome.alarms.clear(name) - // Create new one - chrome.alarms.create(name, { when: Date.now() + config.interval }) + chrome.alarms.clear(name, (_cleared: boolean) => { + // Create new one + chrome.alarms.create(name, { when: nextTs }) + }) }) } From 4c15843f01255bc777fd8ae095b4aad4015c2813 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 9 Feb 2023 19:49:12 +0800 Subject: [PATCH 099/168] Support last 30 days' data on the popup page (#187) and add several localizations --- global.d.ts | 15 ++++++- src/app/components/help-us/progress-list.ts | 11 +++++ src/i18n/message/common/locale.ts | 48 ++++++++++++++++++--- src/i18n/message/common/popup-duration.ts | 4 ++ src/i18n/message/popup/chart.ts | 4 ++ src/popup/components/footer/index.ts | 19 ++++---- src/popup/style/index.sass | 2 +- src/util/constant/popup.ts | 2 +- 8 files changed, 87 insertions(+), 18 deletions(-) diff --git a/global.d.ts b/global.d.ts index 514223c0e..665963661 100644 --- a/global.d.ts +++ b/global.d.ts @@ -6,7 +6,9 @@ */ declare namespace timer { namespace option { - type PopupDuration = "today" | "thisWeek" | "thisMonth" + type PopupDuration = + | "today" | "thisWeek" | "thisMonth" + | "last30Days" /** * Options used for the popup page */ @@ -215,6 +217,17 @@ declare namespace timer { | 'fr' | 'it' | 'sv' + | 'fi' + | 'da' + | 'hr' + | 'id' + | 'tr' + | 'cs' + | 'ro' + | 'nl' + | 'vi' + | 'sk' + | 'mn' namespace stat { /** diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index 53a581343..ac1637355 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -29,6 +29,17 @@ const localeCrowdMap: { [locale in SupportedLocale]: string } = { fr: "fr", it: "it", sv: "sv-SE", + fi: "fi", + da: "da", + hr: "hr", + id: "id", + tr: "tr", + cs: "cs", + ro: "ro", + nl: "nl", + vi: "vi", + sk: "sk", + mn: "mn", } const crowdLocaleMap: { [locale: string]: SupportedLocale } = {} diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index 854cc1cca..3b398ec30 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -10,12 +10,17 @@ * * @since 0.8.0 */ -export type LocaleMessages = { - [locale in timer.Locale | timer.TranslatingLocale]: { - name: string - comma?: string +export type LocaleMessages = + { + [locale in timer.Locale]: { + name: string + comma: string + } + } & { + [translatingLocale in timer.TranslatingLocale]: { + name: string + } } -} const _default: LocaleMessages = { zh_CN: { @@ -67,6 +72,39 @@ const _default: LocaleMessages = { sv: { name: "Sverige" }, + fi: { + name: "Suomalainen", + }, + da: { + name: "dansk", + }, + hr: { + name: "Hrvatski", + }, + id: { + name: "bahasa Indonesia", + }, + tr: { + name: "Türkçe", + }, + cs: { + name: "čeština", + }, + ro: { + name: "Română", + }, + nl: { + name: "Nederlands", + }, + vi: { + name: "Tiếng Việt", + }, + sk: { + name: "slovenský", + }, + mn: { + name: "Монгол", + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration.ts b/src/i18n/message/common/popup-duration.ts index 746f9d3cf..a79846ba7 100644 --- a/src/i18n/message/common/popup-duration.ts +++ b/src/i18n/message/common/popup-duration.ts @@ -12,21 +12,25 @@ const _default: Messages = { today: '今日', thisWeek: '本周', thisMonth: '本月', + last30Days: '近 30 天', }, zh_TW: { today: '今日', thisWeek: '本週', thisMonth: '本月', + last30Days: '近 30 天', }, en: { today: 'Today\'s', thisWeek: 'This Week\'s', thisMonth: 'This Month\'s', + last30Days: 'Last 30 days\'', }, ja: { today: '今日の', thisWeek: '今週の', thisMonth: '今月の', + last30Days: '過去 30 日間', }, } diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index fba3d2972..71a55a4a4 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -28,6 +28,7 @@ const _default: Messages = { today: '今日数据', thisWeek: '本周数据', thisMonth: '本月数据', + last30Days: '近 30 天数据' }, mergeHostLabel: '合并子域名', fileName: '上网时长清单_{today}_by_{app}', @@ -48,6 +49,7 @@ const _default: Messages = { today: '今日數據', thisWeek: '本週數據', thisMonth: '本月數據', + last30Days: '近 30 天數據', }, mergeHostLabel: '合並子網域', fileName: '上網時長清單_{today}_by_{app}', @@ -68,6 +70,7 @@ const _default: Messages = { today: 'Today\'s Data', thisWeek: 'This Week\'s Data', thisMonth: 'This Month\'s Data', + last30Days: 'Last 30 days\' data', }, mergeHostLabel: 'Merge Sites', fileName: 'Web_Time_List_{today}_By_{app}', @@ -88,6 +91,7 @@ const _default: Messages = { today: '今日のデータ', thisWeek: '今週のデータ', thisMonth: '今月のデータ', + last30Days: '過去 30 日間のデータ', }, mergeHostLabel: 'URLをマージ', fileName: 'オンライン時間_{today}_by_{app}', diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 75bd372ac..072fe9485 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -29,11 +29,11 @@ type QueryResultHandler = (result: PopupQueryResult) => void const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: !IS_SAFARI, alias: true } -function calculateDateRange(duration: PopupDuration, weekStart: timer.option.WeekStartOption): Date | Date[] { - const now = new Date() - if (duration == 'today') { - return now - } else if (duration == 'thisWeek') { +type DateRangeCalculator = (now: Date, weekStart: timer.option.WeekStartOption) => Date | [Date, Date] + +const dateRangeCalculators: { [duration in PopupDuration]: DateRangeCalculator } = { + today: now => now, + thisWeek(now, weekStart) { const weekStartAsNormal = !weekStart || weekStart === 'default' if (weekStartAsNormal) { return getWeekTime(now, locale === 'zh_CN') @@ -54,10 +54,9 @@ function calculateDateRange(duration: PopupDuration, weekStart: timer.option.Wee } return [start, now] } - } else if (duration == 'thisMonth') { - const startOfMonth = getMonthTime(now)[0] - return [startOfMonth, now] - } + }, + thisMonth: now => [getMonthTime(now)[0], now], + last30Days: now => [new Date(now.getTime() - MILL_PER_DAY * 29), now], } class FooterWrapper { @@ -135,7 +134,7 @@ class FooterWrapper { getQueryParam(weekStart: timer.option.WeekStartOption): FooterParam { const duration: PopupDuration = this.timeSelectWrapper.getSelectedTime() const param: FooterParam = { - date: calculateDateRange(duration, weekStart), + date: dateRangeCalculators[duration]?.(new Date(), weekStart), mergeHost: this.mergeHostWrapper.mergedHost(), sort: this.typeSelectWrapper.getSelectedType(), sortOrder: 'DESC', diff --git a/src/popup/style/index.sass b/src/popup/style/index.sass index 7f1242636..99978b05c 100644 --- a/src/popup/style/index.sass +++ b/src/popup/style/index.sass @@ -69,7 +69,7 @@ $optionPadding: 10px #time-select-popup z-index: 2004 left: 489px - top: 380px + top: 342px margin: 0px .el-svg-icon diff --git a/src/util/constant/popup.ts b/src/util/constant/popup.ts index 058f66d35..18b4b9581 100644 --- a/src/util/constant/popup.ts +++ b/src/util/constant/popup.ts @@ -5,4 +5,4 @@ * https://opensource.org/licenses/MIT */ -export const ALL_POPUP_DURATION: PopupDuration[] = ["today", "thisWeek", "thisMonth"] \ No newline at end of file +export const ALL_POPUP_DURATION: PopupDuration[] = ["today", "thisWeek", "thisMonth", "last30Days"] \ No newline at end of file From 63ff6aa13de94ccd812b47f5f985aa7d0221e001 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 10 Feb 2023 09:48:42 +0800 Subject: [PATCH 100/168] v1.4.8 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ebf00a1f5..ffd57b0a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.7", + "version": "1.4.8", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} \ No newline at end of file +} From 6864826b8a942b35a86f2f49b9b4e5483ea73540 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 11 Feb 2023 01:21:29 +0800 Subject: [PATCH 101/168] Show translation alert in dashboard --- src/app/components/dashboard/index.ts | 17 +++++--- src/app/components/dashboard/row1.ts | 30 ++++++------- src/app/components/dashboard/row2.ts | 16 +++---- src/app/components/dashboard/row3.ts | 28 ++++++++++++ src/app/components/dashboard/style/index.sass | 6 +++ src/app/layout/menu.ts | 1 - src/i18n/index.ts | 43 ++++++++++++++++--- src/i18n/message/common/locale.ts | 17 +++++--- 8 files changed, 114 insertions(+), 44 deletions(-) create mode 100644 src/app/components/dashboard/row3.ts diff --git a/src/app/components/dashboard/index.ts b/src/app/components/dashboard/index.ts index cb7484268..47e7cb70b 100644 --- a/src/app/components/dashboard/index.ts +++ b/src/app/components/dashboard/index.ts @@ -9,14 +9,21 @@ import { defineComponent, h } from "vue" import ContentContainer from "@app/components/common/content-container" import DashboardRow1 from './row1' import DashboardRow2 from './row2' -import "./style/index" +import DashboardRow3 from './row3' +import "./style" +import { isTranslatingLocale, locale } from "@i18n" const _default = defineComponent({ name: 'Dashboard', - render: () => h(ContentContainer, {}, () => [ - h(DashboardRow1), - h(DashboardRow2) - ]) + render: () => h(ContentContainer, {}, () => { + const nodes = [ + h(DashboardRow1), + h(DashboardRow2), + ] + // Only shows for translating languages' speakers in English + locale === 'en' && isTranslatingLocale() && nodes.push(h(DashboardRow3)) + return nodes + }) }) export default _default \ No newline at end of file diff --git a/src/app/components/dashboard/row1.ts b/src/app/components/dashboard/row1.ts index 7d573ebd0..697f5362e 100644 --- a/src/app/components/dashboard/row1.ts +++ b/src/app/components/dashboard/row1.ts @@ -14,22 +14,20 @@ import TopKVisit from './components/top-k-visit' const _default = defineComponent({ name: "DashboardRow1", - render() { - return h(ElRow, { - gutter: 40, - style: { height: '290px' } - }, () => [ - h(DashboardCard, { - span: 4 - }, () => h(Indicator)), - h(DashboardCard, { - span: 12 - }, () => h(WeekOnWeek)), - h(DashboardCard, { - span: 8 - }, () => h(TopKVisit)), - ]) - } + render: () => h(ElRow, { + gutter: 40, + style: { height: '290px' } + }, () => [ + h(DashboardCard, { + span: 4 + }, () => h(Indicator)), + h(DashboardCard, { + span: 12 + }, () => h(WeekOnWeek)), + h(DashboardCard, { + span: 8 + }, () => h(TopKVisit)), + ]) }) export default _default \ No newline at end of file diff --git a/src/app/components/dashboard/row2.ts b/src/app/components/dashboard/row2.ts index b1fec1e52..91e9648e0 100644 --- a/src/app/components/dashboard/row2.ts +++ b/src/app/components/dashboard/row2.ts @@ -13,16 +13,12 @@ import CalendarHeatMap from './components/calendar-heat-map' const _default = defineComponent({ name: "DashboardRow1", - render() { - return h(ElRow, { - gutter: 40, - style: { height: '280px' } - }, () => [ - h(DashboardCard, { - span: 24 - }, () => h(CalendarHeatMap)) - ]) - } + render: () => h(ElRow, { + gutter: 40, + style: { height: '280px' } + }, () => h(DashboardCard, { + span: 24 + }, () => h(CalendarHeatMap))) }) export default _default \ No newline at end of file diff --git a/src/app/components/dashboard/row3.ts b/src/app/components/dashboard/row3.ts new file mode 100644 index 000000000..8402b4ff3 --- /dev/null +++ b/src/app/components/dashboard/row3.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElRow } from "element-plus" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" + +// Only shows in English +const ALERT_TEXT = '💡 Help us translate this extension/addon into your native language!' + +const _default = defineComponent({ + name: "DashboardRow3", + setup() { + const router = useRouter() + return () => h(ElRow, { + gutter: 40, + }, () => h('span', { + onClick: () => router.push({ path: '/other/help' }), + class: 'help-us-link' + }, ALERT_TEXT)) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/dashboard/style/index.sass b/src/app/components/dashboard/style/index.sass index b44bf7732..d7d3340a7 100644 --- a/src/app/components/dashboard/style/index.sass +++ b/src/app/components/dashboard/style/index.sass @@ -19,6 +19,12 @@ .chart-container height: 100% +.help-us-link + color: var(--el-text-color-primary) + text-align: center + font-size: 14px + width: 100% + cursor: pointer \:root --timer-dashboard-heatmap-color-a: #9be9a8 diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index b6cf29d8b..3afe77570 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -51,7 +51,6 @@ function generateMenus(): _MenuGroup[] { title: 'helpUs', route: '/other/help', icon: HelpFilled, - index: '_i18n' }] HOME_PAGE && otherMenuItems.push({ title: 'rate', diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 54a6a3b19..e724ffcfb 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -18,17 +18,39 @@ const FEEDBACK_LOCALE: timer.Locale = "en" export const defaultLocale: timer.Locale = "zh_CN" -// export type Messages = { -// [key in timer.Locale]: T -// } - // Standardize the locale code according to the Chrome locale code const chrome2I18n: { [key: string]: timer.Locale } = { 'zh-CN': "zh_CN", 'zh-TW': "zh_TW", 'en-US': "en", 'en-GB': "en", - 'ja': "ja" + 'ja': "ja", +} + +const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { + de: 'de', + "es-ES": 'es', + "es-MX": 'es', + ko: 'ko', + pl: 'pl', + "pt-PT": 'pt', + "pt-BR": 'pt_BR', + ru: 'ru', + uk: 'uk', + fr: 'fr', + it: 'it', + sv: 'sv', + fi: 'fi', + da: 'da', + hr: 'hr', + id: 'id', + tr: 'tr', + cs: 'cs', + ro: 'ro', + nl: 'nl', + vi: 'vi', + sk: 'sk', + mn: 'mn', } /** @@ -51,6 +73,17 @@ export function chromeLocale2ExtensionLocale(chromeLocale: string): timer.Locale */ export let localeSameAsBrowser: timer.Locale = chromeLocale2ExtensionLocale(chrome.i18n.getUILanguage()) +/** + * @since 1.5.0 + */ +export function isTranslatingLocale(): boolean { + const uiLang = chrome.i18n.getUILanguage() + if (chrome2I18n[uiLang]) { + return false + } + return !!translationChrome2I18n[uiLang] +} + /** * Real locale with locale option */ diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index 3b398ec30..135fa455d 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -5,6 +5,14 @@ * https://opensource.org/licenses/MIT */ +type MetaBase = { + name: string +} + +type Meta = MetaBase & { + comma: string +} + /** * Meta info of locales * @@ -12,14 +20,9 @@ */ export type LocaleMessages = { - [locale in timer.Locale]: { - name: string - comma: string - } + [locale in timer.Locale]: Meta } & { - [translatingLocale in timer.TranslatingLocale]: { - name: string - } + [translatingLocale in timer.TranslatingLocale]: MetaBase } const _default: LocaleMessages = { From bc9d1673b30bf42415936a11225b24ae9bf142e3 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 11 Feb 2023 01:21:50 +0800 Subject: [PATCH 102/168] v1.5.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ffd57b0a0..cd20d9305 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.4.8", + "version": "1.5.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} +} \ No newline at end of file From 903a38404e47d5e4edab776667120bb8a88e1120 Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 13 Feb 2023 09:55:11 +0800 Subject: [PATCH 103/168] v1.5.1.edge-chrome --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cd20d9305..4a28c5eeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.5.0", + "version": "1.5.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} \ No newline at end of file +} From a2b009ecfe7b8da5e2ab1f06f300970d0f2f0cd7 Mon Sep 17 00:00:00 2001 From: ZHY Date: Tue, 14 Feb 2023 16:01:05 +0800 Subject: [PATCH 104/168] Fix NPE --- src/util/site.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 +} From 584a7d62e9c8b98a4a32ff63355d5b9dd4c2dbc2 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 15 Feb 2023 23:09:42 +0800 Subject: [PATCH 105/168] Remove log of messager --- global.d.ts | 2 +- src/background/message-dispatcher.ts | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/global.d.ts b/global.d.ts index 665963661..5ef2144b7 100644 --- a/global.d.ts +++ b/global.d.ts @@ -550,7 +550,7 @@ declare namespace timer { /** * @since 1.3.0 */ - type Handler = (data: Req) => Promise + type Handler = (data: Req, sender: chrome.runtime.MessageSender) => Promise /** * @since 0.8.4 */ diff --git a/src/background/message-dispatcher.ts b/src/background/message-dispatcher.ts index 42dc973cb..1f8cab742 100644 --- a/src/background/message-dispatcher.ts +++ b/src/background/message-dispatcher.ts @@ -5,8 +5,6 @@ * https://opensource.org/licenses/MIT */ -import { log } from "@src/common/logger" - class MessageDispatcher { private handlers: Partial<{ [code in timer.mq.ReqCode]: timer.mq.Handler @@ -20,7 +18,7 @@ class MessageDispatcher { return this } - private async handle(message: timer.mq.Request): Promise> { + private async handle(message: timer.mq.Request, sender: chrome.runtime.MessageSender): Promise> { const code = message?.code if (!code) { return { code: 'ignore' } @@ -30,7 +28,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 } @@ -40,12 +38,8 @@ 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) - }) + chrome.runtime.onMessage.addListener((message: timer.mq.Request, sender: chrome.runtime.MessageSender, sendResponse: timer.mq.Callback) => { + this.handle(message, sender).then(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 From 985fa3f64dca336837c9124e861bd389c31edbe6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 20 Feb 2023 19:30:17 +0800 Subject: [PATCH 106/168] Refactor api of chrome --- global.d.ts | 19 +++++++ src/api/chrome/action.ts | 11 ++++ src/api/chrome/alarm.ts | 18 ++++++ src/api/chrome/common.ts | 8 +++ src/api/chrome/context-menu.ts | 15 +++++ src/{ => api/chrome}/permissions.ts | 0 src/api/chrome/runtime.ts | 41 +++++++++++++ src/api/chrome/tab.ts | 53 +++++++++++++++++ src/api/chrome/window.ts | 45 +++++++++++++++ .../dashboard/components/calendar-heat-map.ts | 3 +- src/app/components/limit/modify/form/url.ts | 2 +- src/app/layout/menu.ts | 5 +- src/background/active-tab-listener.ts | 8 ++- src/background/alarm-manager.ts | 19 ++++--- src/background/badge-text-manager.ts | 38 ++++--------- src/background/browser-action-menu-manager.ts | 57 +++++++++---------- src/background/icon-and-alias-collector.ts | 8 ++- src/background/index.ts | 20 +++---- src/background/limit-processor.ts | 29 +++------- src/background/message-dispatcher.ts | 13 ++--- src/background/timer/collector.ts | 16 ++---- src/background/timer/save.ts | 22 +++---- src/background/uninstall-listener.ts | 3 +- .../1-4-3/running-time-clear.ts | 2 +- src/background/version-manager/index.ts | 7 ++- .../version-manager/version-manager.d.ts | 2 +- src/background/whitelist-menu-manager.ts | 32 ++++++----- src/content-script/index.ts | 39 ++----------- src/content-script/limit.ts | 55 ++++-------------- src/content-script/printer.ts | 14 +---- src/guide/component/common.ts | 3 +- src/popup/components/chart/click-handler.ts | 3 +- src/popup/components/chart/option.ts | 5 +- src/popup/components/footer/all-function.ts | 5 +- src/popup/components/footer/upgrade.ts | 3 +- src/service/limit-service.ts | 15 ++--- src/util/constant/environment.ts | 2 + src/util/constant/meta.ts | 3 +- src/util/constant/url.ts | 5 +- 39 files changed, 374 insertions(+), 274 deletions(-) create mode 100644 src/api/chrome/action.ts create mode 100644 src/api/chrome/alarm.ts create mode 100644 src/api/chrome/common.ts create mode 100644 src/api/chrome/context-menu.ts rename src/{ => api/chrome}/permissions.ts (100%) create mode 100644 src/api/chrome/runtime.ts create mode 100644 src/api/chrome/tab.ts create mode 100644 src/api/chrome/window.ts diff --git a/global.d.ts b/global.d.ts index 5ef2144b7..d64b9babd 100644 --- a/global.d.ts +++ b/global.d.ts @@ -557,3 +557,22 @@ declare namespace timer { type Callback = (result?: Response) => void } } + +/** + * 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/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/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 1bdc5e277..b7216e3ca 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -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 { 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/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..101e10063 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -5,6 +5,9 @@ * https://opensource.org/licenses/MIT */ +import { setBadgeText } from "@api/chrome/action" +import { listTabs } from "@api/chrome/tab" +import { getFocusedNormalWindow } from "@api/chrome/window" import TimerDatabase from "@db/timer-database" import whitelistHolder from "@service/components/whitelist-holder" import optionService from "@service/option-service" @@ -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 { diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 0d4fb41bc..28f3f34d8 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -8,15 +8,18 @@ import { OPTION_ROUTE } from "../app/router/constants" import { getAppPageUrl, getGuidePageUrl, 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" 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 +32,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(TU_CAO_PAGE), ...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/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index 499ceae66..e0e0a27b9 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -13,6 +13,7 @@ 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" const storage: chrome.storage.StorageArea = chrome.storage.local const iconUrlDatabase = new IconUrlDatabase(storage) @@ -38,7 +39,7 @@ function collectAlias(host: string, tabTitle: string) { /** * 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 @@ -58,12 +59,13 @@ async function processTabInfo(tab: chrome.tabs.Tab): Promise { /** * 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 1f8cab742..0362974cb 100644 --- a/src/background/message-dispatcher.ts +++ b/src/background/message-dispatcher.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import { onRuntimeMessage } from "@api/chrome/runtime" + class MessageDispatcher { private handlers: Partial<{ [code in timer.mq.ReqCode]: timer.mq.Handler @@ -18,7 +20,7 @@ class MessageDispatcher { return this } - private async handle(message: timer.mq.Request, sender: chrome.runtime.MessageSender): Promise> { + private async handle(message: timer.mq.Request, sender: ChromeMessageSender): Promise> { const code = message?.code if (!code) { return { code: 'ignore' } @@ -36,14 +38,7 @@ class MessageDispatcher { } start() { - // 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) => { - this.handle(message, sender).then(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 - }) + 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..c5d40820d 100644 --- a/src/background/timer/save.ts +++ b/src/background/timer/save.ts @@ -5,27 +5,19 @@ * 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 { 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) { 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/1-4-3/running-time-clear.ts b/src/background/version-manager/1-4-3/running-time-clear.ts index cb7b9a1de..07d157814 100644 --- a/src/background/version-manager/1-4-3/running-time-clear.ts +++ b/src/background/version-manager/1-4-3/running-time-clear.ts @@ -14,7 +14,7 @@ export default class RunningTimeClear implements VersionProcessor { return "1.4.3" } - async process(reason: chrome.runtime.OnInstalledReason): Promise { + async process(reason: ChromeOnInstalledReason): Promise { // Only trigger when updating if (reason !== 'update') { return diff --git a/src/background/version-manager/index.ts b/src/background/version-manager/index.ts index 9378ccda1..f1a01dc2f 100644 --- a/src/background/version-manager/index.ts +++ b/src/background/version-manager/index.ts @@ -5,6 +5,7 @@ * 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" @@ -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/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/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/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..779e4403d 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -22,6 +22,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 @@ -208,9 +209,7 @@ 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' })) } } } 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/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/limit-service.ts b/src/service/limit-service.ts index 8002a00b6..f4d980e5a 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) - }) - })) + limitedItems?.length && sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) + .catch(err => console.log(err.message)) + }) } async function updateEnabled(item: timer.limit.Item): Promise { 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..20dcc93aa 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/' @@ -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 */ From 2964bf9406e4da1fa33a0acae8d3625372cf5f30 Mon Sep 17 00:00:00 2001 From: ZHY Date: Thu, 23 Feb 2023 20:12:27 +0800 Subject: [PATCH 107/168] Fix limit bugs --- src/service/limit-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service/limit-service.ts b/src/service/limit-service.ts index f4d980e5a..ae40032fb 100644 --- a/src/service/limit-service.ts +++ b/src/service/limit-service.ts @@ -47,7 +47,7 @@ async function handleLimitChanged() { const tabs = await listTabs() tabs.forEach(tab => { const limitedItems = allItems.filter(item => item.matches(tab.url) && item.enabled && item.hasLimited()) - limitedItems?.length && sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) + sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) .catch(err => console.log(err.message)) }) } @@ -134,4 +134,4 @@ class LimitService { } } -export default new LimitService() \ No newline at end of file +export default new LimitService() From 44e9518a1246da8e47dbbbc69452d481f4de3ee1 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 00:26:31 +0800 Subject: [PATCH 108/168] Omit long labels of x-axis (#189) --- .../components/dashboard/components/week-on-week.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/components/dashboard/components/week-on-week.ts b/src/app/components/dashboard/components/week-on-week.ts index 0618a518c..9a93c0da6 100644 --- a/src/app/components/dashboard/components/week-on-week.ts +++ b/src/app/components/dashboard/components/week-on-week.ts @@ -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: { From e98d65d812367b7d2d266ae85538c12cfe713933 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 00:28:31 +0800 Subject: [PATCH 109/168] v1.5.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4a28c5eeb..ee467c56b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.5.1", + "version": "1.5.2", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} +} \ No newline at end of file From defb75022276f9316af3eea01a756efd056f23f6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 00:59:14 +0800 Subject: [PATCH 110/168] Support optional locales for development --- global.d.ts | 24 +++++++++++++++---- script/crowdin/common.ts | 1 - src/app/components/option/common.ts | 6 ++++- src/i18n/i18n.d.ts | 9 ++++++-- src/i18n/index.ts | 16 +++++++++++-- src/i18n/message/app/help-us.ts | 36 ++--------------------------- 6 files changed, 47 insertions(+), 45 deletions(-) diff --git a/global.d.ts b/global.d.ts index d64b9babd..1d132a835 100644 --- a/global.d.ts +++ b/global.d.ts @@ -190,15 +190,20 @@ declare namespace timer { * @since 1.4.0 */ type SourceLocale = 'en' - /** - * @since 0.8.0 + * The locale must be translated with code + * + * @since 1.5.3 */ - type Locale = SourceLocale - | 'zh_CN' + type RequiredLocale = SourceLocale | 'zh_CN' + type OptionalLocale = | 'ja' // @since 0.9.0 | 'zh_TW' + /** + * @since 0.8.0 + */ + type Locale = RequiredLocale | OptionalLocale /** * Translating locales @@ -575,4 +580,13 @@ 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 +declare type ChromeMessageHandler = (req: timer.mq.Request, sender: ChromeMessageSender) => Promise> + +// Embedded partial +declare type EmbeddedPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : EmbeddedPartial; +} diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 1f5dc232c..513929406 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -71,7 +71,6 @@ export async function readAllMessages(dir: Dir): Promise 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/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..806ff4490 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: '正在查询翻译进度...', @@ -41,39 +41,7 @@ const _default: Messages = { 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.', - }, - 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.', + 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...', From e9906fe64fe82265473d338f46b213f5cd2dc93c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 10:20:55 +0800 Subject: [PATCH 111/168] Optimize crowdin --- script/crowdin/client.ts | 24 ++++++---------- script/crowdin/common.ts | 15 ++++++++-- script/crowdin/export-translation.ts | 2 +- src/i18n/message/app/help-us.ts | 11 ++------ src/i18n/message/app/menu.ts | 4 +-- src/i18n/message/app/merge-rule.ts | 4 +-- src/i18n/message/app/option.ts | 8 +++--- src/i18n/message/app/report.ts | 6 ++-- src/i18n/message/common/context-menus.ts | 4 +-- src/i18n/message/guide/layout.ts | 2 +- src/i18n/message/guide/privacy.ts | 4 +-- src/i18n/message/guide/profile.ts | 2 +- src/i18n/message/guide/usage.ts | 36 +++++++++++------------- src/i18n/message/popup/chart.ts | 2 +- 14 files changed, 59 insertions(+), 65 deletions(-) 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 513929406..75b3b3a19 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -93,6 +93,10 @@ export async function mergeMessage( existMessages[locale] = existMessage = {} } 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}`) @@ -148,9 +152,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) @@ -159,6 +163,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) @@ -200,7 +206,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 diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts index b2b08df91..c115aa069 100644 --- a/script/crowdin/export-translation.ts +++ b/script/crowdin/export-translation.ts @@ -6,7 +6,7 @@ async function processFile(client: CrowdinClient, file: SourceFilesModel.File, d const itemSets: Partial> = {} for (const locale of ALL_TRANS_LOCALES) { const lang = crowdinLangOf(locale) - const items: ItemSet = await client.downloadTranslations(file.id, lang) + const items: any = await client.downloadTranslations(file.id, lang) itemSets[locale] = items } await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index 806ff4490..22d9938af 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -33,14 +33,9 @@ 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.', + 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', diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index ccf04f374..b3be6ec77 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -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/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}', From ed99d1d1e383478c8e80d1da5604bff6f724d4c7 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 10:58:35 +0800 Subject: [PATCH 112/168] Fix for optional locales --- script/crowdin/sync-translation.ts | 4 ++++ 1 file changed, 4 insertions(+) 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) From 8c848385bc40016590457eeaf09e245b0b353af0 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 11:01:00 +0800 Subject: [PATCH 113/168] Change the process threshold of translations --- src/app/components/help-us/progress-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From d64f5b70aba09a76ade18f407110678eece0b786 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 11:10:08 +0800 Subject: [PATCH 114/168] Fix export --- script/crowdin/common.ts | 9 +++++---- script/crowdin/export-translation.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 75b3b3a19..f21f86bc4 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -88,10 +88,7 @@ export async function mergeMessage( } const sourceItemSet = transMsg(existMessages[SOURCE_LOCALE]) Object.entries(messages).forEach(([locale, itemSet]) => { - 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 @@ -105,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' }) diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts index c115aa069..a44193e4d 100644 --- a/script/crowdin/export-translation.ts +++ b/script/crowdin/export-translation.ts @@ -6,8 +6,8 @@ async function processFile(client: CrowdinClient, file: SourceFilesModel.File, d const itemSets: Partial> = {} for (const locale of ALL_TRANS_LOCALES) { const lang = crowdinLangOf(locale) - const items: any = await client.downloadTranslations(file.id, lang) - itemSets[locale] = items + const items: ItemSet = await client.downloadTranslations(file.id, lang) + items && Object.keys(items).length && (itemSets[locale] = items) } await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) } From 4f5f84114800ea5200442b965159dd7ab091a1b3 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 11:10:27 +0800 Subject: [PATCH 115/168] Export translations from Crowdin --- src/i18n/message/app/help-us.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index 22d9938af..d6e6cc564 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -41,6 +41,10 @@ const _default: Messages = { button: 'Go Crowdin', loading: 'Checking translation progress...', }, + zh_TW: { + button: '前往 Crowdin', + loading: '正在檢查翻譯進度...', + }, } export default _default \ No newline at end of file From 92dfb69c67e5f394c8452a4aef7cd7babfafc1c0 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Feb 2023 13:17:04 +0800 Subject: [PATCH 116/168] Split declarations --- types/chrome.d.ts | 18 +++++++++++++++ types/common.d.ts | 8 +++++++ global.d.ts => types/timer.d.ts | 41 +++++---------------------------- 3 files changed, 32 insertions(+), 35 deletions(-) create mode 100644 types/chrome.d.ts create mode 100644 types/common.d.ts rename global.d.ts => types/timer.d.ts (92%) 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/global.d.ts b/types/timer.d.ts similarity index 92% rename from global.d.ts rename to types/timer.d.ts index 1d132a835..d08cb0808 100644 --- a/global.d.ts +++ b/types/timer.d.ts @@ -1,10 +1,9 @@ - -/** - * The options - * - * @since 0.3.0 - */ declare namespace timer { + /** + * The options + * + * @since 0.3.0 + */ namespace option { type PopupDuration = | "today" | "thisWeek" | "thisMonth" @@ -561,32 +560,4 @@ declare namespace timer { */ type Callback = (result?: Response) => void } -} - -/** - * 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> - -// Embedded partial -declare type EmbeddedPartial = { - [P in keyof T]?: T[P] extends Array - ? Array> - : T[P] extends ReadonlyArray - ? ReadonlyArray> - : EmbeddedPartial; -} +} \ No newline at end of file From d076c36e325f37d2a00231453ae88b72ea1181e5 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 16 Mar 2023 23:21:59 +0800 Subject: [PATCH 117/168] Support virtual sites (#194, #191) --- package.json | 2 +- script/crowdin/common.ts | 10 - .../alias-info.ts => common/editable.ts} | 4 +- src/app/components/common/host-alert.ts | 17 +- .../dashboard/components/calendar-heat-map.ts | 6 +- .../dashboard/components/indicator/index.ts | 4 +- .../dashboard/components/top-k-visit.ts | 8 +- .../dashboard/components/week-on-week.ts | 11 +- .../clear/filter/operation-button.ts | 10 +- src/app/components/data-manage/clear/index.ts | 12 +- src/app/components/report/filter/index.ts | 4 +- src/app/components/report/index.ts | 34 +- .../components/report/table/columns/alias.ts | 4 +- .../report/table/columns/operation.ts | 10 +- src/app/components/site-manage/common.ts | 42 +- src/app/components/site-manage/index.ts | 14 +- .../{name-form-item.ts => alias-form-item.ts} | 4 +- .../site-manage/modify/host-form-item.ts | 84 +-- .../components/site-manage/modify/index.ts | 87 +-- .../site-manage/table/column/alias.ts | 17 +- .../site-manage/table/column/host.ts | 17 +- .../site-manage/table/column/icon.ts | 21 + .../site-manage/table/column/operation.ts | 31 +- .../site-manage/table/column/source.ts | 2 +- .../site-manage/table/column/type.ts | 48 ++ src/app/components/site-manage/table/index.ts | 16 +- .../trend/components/chart/wrapper.ts | 6 +- src/app/components/trend/components/common.ts | 8 +- src/app/components/trend/components/filter.ts | 27 +- src/app/components/trend/index.ts | 6 +- src/app/components/trend/trend.d.ts | 3 +- src/background/badge-text-manager.ts | 6 +- src/background/content-script-handler.ts | 6 +- src/background/icon-and-alias-collector.ts | 19 +- src/background/timer/save.ts | 4 +- src/background/timer/timer.d.ts | 2 +- .../0-7-0/local-file-initializer.ts | 44 +- .../1-4-3/running-time-clear.ts | 38 -- .../1-6-0/alias-icon-cleaner.ts | 46 ++ src/background/version-manager/index.ts | 2 +- src/common/backup/processor.ts | 8 +- src/database/host-alias-database.ts | 93 +-- src/database/icon-url-database.ts | 40 +- src/database/meta-database.ts | 8 +- src/database/site-database.ts | 199 +++++++ .../{timer-database.ts => stat-database.ts} | 44 +- src/guide/layout/content.ts | 1 - src/i18n/message/app/menu.ts | 4 +- src/i18n/message/app/site-manage.ts | 42 +- src/i18n/message/app/trend.ts | 5 + src/package.ts | 13 +- src/popup/components/chart/option.ts | 41 +- src/popup/components/footer/index.ts | 13 +- src/service/components/host-merge-ruler.ts | 21 +- src/service/components/immigration.ts | 8 +- src/service/components/virtual-site-holder.ts | 63 ++ src/service/host-alias-service.ts | 62 -- src/service/meta-service.ts | 6 +- src/service/site-service.ts | 96 +++ src/service/stat-service/index.ts | 246 ++++++++ .../{timer-service => stat-service}/merge.ts | 1 + src/service/stat-service/remote.ts | 91 +++ src/service/timer-service/index.ts | 323 ---------- src/util/constant/url.ts | 3 +- src/util/pattern.ts | 40 ++ src/util/stat.ts | 8 +- .../background/backup/gist/compressor.test.ts | 6 +- test/database/icon-url-database.test.ts | 47 -- test/database/timer-database.test.ts | 12 +- test/util/pattern.test.ts | 21 +- types/json.d.ts | 4 - types/timer.d.ts | 563 ------------------ types/timer/app.d.ts | 10 + types/timer/backup.d.ts | 32 + types/timer/common.d.ts | 15 + types/timer/index.d.ts | 71 +++ types/timer/limit.d.ts | 47 ++ types/timer/merge.d.ts | 18 + types/timer/mq.d.ts | 45 ++ types/timer/option.d.ts | 161 +++++ types/timer/period.d.ts | 32 + types/timer/site.d.ts | 29 + types/timer/stat.d.ts | 101 ++++ webpack/webpack.common.ts | 2 +- webpack/webpack.prod.ts | 2 +- 85 files changed, 1904 insertions(+), 1529 deletions(-) rename src/app/components/{report/table/columns/alias-info.ts => common/editable.ts} (97%) rename src/app/components/site-manage/modify/{name-form-item.ts => alias-form-item.ts} (91%) create mode 100644 src/app/components/site-manage/table/column/icon.ts create mode 100644 src/app/components/site-manage/table/column/type.ts delete mode 100644 src/background/version-manager/1-4-3/running-time-clear.ts create mode 100644 src/background/version-manager/1-6-0/alias-icon-cleaner.ts create mode 100644 src/database/site-database.ts rename src/database/{timer-database.ts => stat-database.ts} (88%) create mode 100644 src/service/components/virtual-site-holder.ts delete mode 100644 src/service/host-alias-service.ts create mode 100644 src/service/site-service.ts create mode 100644 src/service/stat-service/index.ts rename src/service/{timer-service => stat-service}/merge.ts (99%) create mode 100644 src/service/stat-service/remote.ts delete mode 100644 src/service/timer-service/index.ts delete mode 100644 test/database/icon-url-database.test.ts delete mode 100644 types/json.d.ts delete mode 100644 types/timer.d.ts create mode 100644 types/timer/app.d.ts create mode 100644 types/timer/backup.d.ts create mode 100644 types/timer/common.d.ts create mode 100644 types/timer/index.d.ts create mode 100644 types/timer/limit.d.ts create mode 100644 types/timer/merge.d.ts create mode 100644 types/timer/mq.d.ts create mode 100644 types/timer/option.d.ts create mode 100644 types/timer/period.d.ts create mode 100644 types/timer/site.d.ts create mode 100644 types/timer/stat.d.ts diff --git a/package.json b/package.json index ee467c56b..d6da5ced7 100644 --- a/package.json +++ b/package.json @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} \ No newline at end of file +} diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index f21f86bc4..736ce1503 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -223,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/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 b7216e3ca..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" @@ -269,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 9a93c0da6..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" @@ -197,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/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/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 101e10063..7690f7b12 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -8,14 +8,14 @@ import { setBadgeText } from "@api/chrome/action" import { listTabs } from "@api/chrome/tab" import { getFocusedNormalWindow } from "@api/chrome/window" -import TimerDatabase from "@db/timer-database" +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 = { /** @@ -76,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/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 e0e0a27b9..b80cb70c0 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -5,8 +5,6 @@ * 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" @@ -14,10 +12,9 @@ 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 @@ -29,11 +26,11 @@ 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') } /** @@ -51,9 +48,13 @@ async function processTabInfo(tab: ChromeTab): 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) } /** diff --git a/src/background/timer/save.ts b/src/background/timer/save.ts index c5d40820d..74d6ae1be 100644 --- a/src/background/timer/save.ts +++ b/src/background/timer/save.ts @@ -8,7 +8,7 @@ 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" @@ -24,7 +24,7 @@ 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/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 07d157814..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: ChromeOnInstalledReason): 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 f1a01dc2f..87eb5b534 100644 --- a/src/background/version-manager/index.ts +++ b/src/background/version-manager/index.ts @@ -8,7 +8,7 @@ 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 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/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/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/message/app/menu.ts b/src/i18n/message/app/menu.ts index b3be6ec77..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: '擴充選項', 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/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/option.ts b/src/popup/components/chart/option.ts index 779e4403d..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" @@ -32,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 = [] @@ -83,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}}` } @@ -214,18 +228,15 @@ export function pieOptions(props: ChartProps, container: HTMLDivElement): EcOpti } } } - 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/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/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/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/url.ts b/src/util/constant/url.ts index 20dcc93aa..af264e2b0 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -112,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/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/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.d.ts b/types/timer.d.ts deleted file mode 100644 index d08cb0808..000000000 --- a/types/timer.d.ts +++ /dev/null @@ -1,563 +0,0 @@ -declare namespace timer { - /** - * The options - * - * @since 0.3.0 - */ - 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' - /** - * 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' - - 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, 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/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') From 4c8b4a31079241fe199593060265a8ba0a0945d7 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 16 Mar 2023 23:34:29 +0800 Subject: [PATCH 118/168] Jump to github issue if user language is not zh_CN (#194) --- src/background/browser-action-menu-manager.ts | 5 +++-- src/util/constant/url.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 28f3f34d8..7e288d679 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -6,12 +6,13 @@ */ 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_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) @@ -56,7 +57,7 @@ const repoPageProps: ChromeContextMenuCreateProps = { const feedbackPageProps: ChromeContextMenuCreateProps = { id: getRuntimeId() + '_timer_menu_item_feedback_link', title: titleOf('😿', t2Chrome(msg => msg.contextMenus.feedbackPage)), - onclick: () => createTab(TU_CAO_PAGE), + onclick: () => createTab(locale === 'zh_CN' ? TU_CAO_PAGE : GITHUB_ISSUE_ADD), ...baseProps } diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index af264e2b0..d4e5aaaac 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -34,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 From f087c174371720aa5f435313db0152da2393346d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 16 Mar 2023 23:34:52 +0800 Subject: [PATCH 119/168] v1.6.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d6da5ced7..ba88d5f03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.5.2", + "version": "1.6.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -60,4 +60,4 @@ "vue": "^3.2.45", "vue-router": "^4.1.6" } -} +} \ No newline at end of file From 56c2d7200b21e7847b22d5fa71a8df8b91358adb Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 19 Mar 2023 21:20:20 +0800 Subject: [PATCH 120/168] Exclusive virtual sites in dashboard --- .../dashboard/components/indicator/index.ts | 61 +++++++++---------- src/database/stat-database.ts | 8 +++ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/app/components/dashboard/components/indicator/index.ts b/src/app/components/dashboard/components/indicator/index.ts index 1cf3b0097..b76b66970 100644 --- a/src/app/components/dashboard/components/indicator/index.ts +++ b/src/app/components/dashboard/components/indicator/index.ts @@ -36,7 +36,7 @@ function calculateInstallDays(installTime: Date, now: Date): number { } async function query(): Promise<_Value> { - const allData: timer.stat.Row[] = await statService.select() + const allData: timer.stat.Row[] = await statService.select({ exlcusiveVirtual: true }) const hostSet = new Set() let visits = 0 let browsingTime = 0 @@ -135,37 +135,34 @@ function renderMostUse(most2Hour: number) { ) } -const _default = defineComponent({ - name: "Indicator", - setup() { - const installedDays: Ref = ref() - const siteCount: Ref = ref(0) - const visitCount: Ref = ref(0) - const browsingMinutes: Ref = ref(0) - const most2Hour: Ref = ref(0) - query().then(value => { - installedDays.value = value.installedDays - siteCount.value = value.sites - visitCount.value = value.visits - browsingMinutes.value = Math.floor(value.browsingTime / MILL_PER_MINUTE) - most2Hour.value = value.most2Hour - }) - return () => { - const items = [ - renderVisits(siteCount.value, visitCount.value), - renderBrowsingMinute(browsingMinutes.value), - renderMostUse(most2Hour.value) - ] - const installedDaysVal = installedDays.value - installedDaysVal && items.splice(0, 0, renderInstalledDays(installedDaysVal)) - return h('div', { - id: CONTAINER_ID, - class: 'chart-container' - }, [ - h(IndicatorHeaderIcon), - ...items - ]) - } +const _default = defineComponent(() => { + const installedDays: Ref = ref() + const siteCount: Ref = ref(0) + const visitCount: Ref = ref(0) + const browsingMinutes: Ref = ref(0) + const most2Hour: Ref = ref(0) + query().then(value => { + installedDays.value = value.installedDays + siteCount.value = value.sites + visitCount.value = value.visits + browsingMinutes.value = Math.floor(value.browsingTime / MILL_PER_MINUTE) + most2Hour.value = value.most2Hour + }) + return () => { + const items = [ + renderVisits(siteCount.value, visitCount.value), + renderBrowsingMinute(browsingMinutes.value), + renderMostUse(most2Hour.value) + ] + const installedDaysVal = installedDays.value + installedDaysVal && items.splice(0, 0, renderInstalledDays(installedDaysVal)) + return h('div', { + id: CONTAINER_ID, + class: 'chart-container' + }, [ + h(IndicatorHeaderIcon), + ...items + ]) } }) diff --git a/src/database/stat-database.ts b/src/database/stat-database.ts index 6ba2fb632..0cdc99ccd 100644 --- a/src/database/stat-database.ts +++ b/src/database/stat-database.ts @@ -40,6 +40,12 @@ export type StatCondition = { * @since 0.0.8 */ fullHost?: boolean + /** + * Whether to exlcusive virtual sites + * + * @since 1.6.1 + */ + exlcusiveVirtual?: boolean } type _StatCondition = StatCondition & { @@ -203,9 +209,11 @@ class StatDatabase extends BaseDatabase { private filterHost(host: string, condition: _StatCondition): boolean { const paramHost = (condition.host || '').trim() + const exlcusiveVirtual = condition.exlcusiveVirtual if (!paramHost) return true if (!!condition.fullHost && host !== paramHost) return false if (!condition.fullHost && !host.includes(paramHost)) return false + if (exlcusiveVirtual && judgeVirtualFast(host)) return false return true } From df3d9f4ce48f4a7aa7d4f29edff8dc9f2af78dee Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 19 Mar 2023 21:20:35 +0800 Subject: [PATCH 121/168] upgrade dependencies --- package.json | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index ba88d5f03..5809719fc 100644 --- a/package.json +++ b/package.json @@ -15,49 +15,52 @@ "author": "zhy", "license": "MIT", "devDependencies": { - "@crowdin/crowdin-api-client": "^1.19.2", - "@types/chrome": "0.0.205", + "@crowdin/crowdin-api-client": "^1.22.1", + "@types/chrome": "0.0.224", "@types/copy-webpack-plugin": "^8.0.1", "@types/echarts": "^4.9.16", "@types/generate-json-webpack-plugin": "^0.3.4", - "@types/jest": "^29.2.4", - "@types/node": "^18.11.17", + "@types/jest": "^29.5.0", + "@types/node": "^18.15.3", "@types/psl": "^1.1.0", "@types/webpack": "^5.28.0", "@types/webpack-bundle-analyzer": "^4.6.0", - "babel-loader": "^9.1.0", + "babel-loader": "^9.1.2", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.3", - "eslint": "^8.30.0", + "eslint": "^8.36.0", "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "mini-css-extract-plugin": "^2.7.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "mini-css-extract-plugin": "^2.7.5", "node-sass": "^8.0.0", - "sass-loader": "^13.2.0", - "style-loader": "^3.3.1", - "ts-jest": "^29.0.3", + "sass-loader": "^13.2.1", + "style-loader": "^3.3.2", + "ts-jest": "^29.0.5", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", - "tsconfig-paths": "^4.1.1", - "tslib": "^2.4.1", - "typescript": "4.9.4", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.5.0", + "typescript": "5.0.2", "url-loader": "^4.1.1", - "webpack": "^5.75.0", - "webpack-bundle-analyzer": "^4.7.0", + "webpack": "^5.76.2", + "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^5.0.1" }, "dependencies": { - "@element-plus/icons-vue": "^2.0.10", - "axios": "^1.2.1", + "@element-plus/icons-vue": "^2.1.0", + "axios": "^1.3.4", "clipboardy": "^3.0.0", - "countup.js": "^2.3.2", + "countup.js": "^2.6.0", "echarts": "^5.4.1", - "element-plus": "2.2.27", + "element-plus": "2.3.1", "psl": "^1.9.0", "stream-browserify": "^3.0.0", - "vue": "^3.2.45", + "vue": "^3.2.47", "vue-router": "^4.1.6" + }, + "engines": { + "node": ">=16" } } \ No newline at end of file From 50382d93728324501ceb263d515beb2ccfe3571d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 23 Mar 2023 16:03:27 +0800 Subject: [PATCH 122/168] Fix visit stat error of virtual sites --- src/background/content-script-handler.ts | 16 ++++++++++------ src/content-script/index.ts | 2 +- src/service/stat-service/index.ts | 17 +++++++++-------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/background/content-script-handler.ts b/src/background/content-script-handler.ts index 99d079a86..69b20a560 100644 --- a/src/background/content-script-handler.ts +++ b/src/background/content-script-handler.ts @@ -20,8 +20,15 @@ import MessageDispatcher from "./message-dispatcher" export default function init(dispatcher: MessageDispatcher) { dispatcher // Increase the visit time - .register('cs.incVisitCount', async host => { - statService.addOneTime(host) + .register('cs.incVisitCount', async (param) => { + let host: string, url: string = undefined + if (typeof param === 'string') { + host = param + } else { + host = param?.host + url = param?.url + } + statService.addOneTime(host, url) }) // Judge is in whitelist .register('cs.isInWhitelist', host => whitelistService.include(host)) @@ -31,10 +38,7 @@ export default function init(dispatcher: MessageDispatcher) { return !!option.printInConsole }) // Get today info - .register('cs.getTodayInfo', host => { - const now = new Date() - return statService.getResult(host, now) - }) + .register('cs.getTodayInfo', host => statService.getResult(host, new Date())) // More minutes .register('cs.moreMinutes', url => limitService.moreMinutes(url)) // cs.getLimitedRules diff --git a/src/content-script/index.ts b/src/content-script/index.ts index d155a6489..0f53c2956 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -19,7 +19,7 @@ async function main() { const isWhitelist = await sendMsg2Runtime('cs.isInWhitelist', host) if (isWhitelist) return - sendMsg2Runtime('cs.incVisitCount', host) + sendMsg2Runtime('cs.incVisitCount', { host, url }) await initLocale() const needPrintInfo = await sendMsg2Runtime('cs.printTodayInfo') diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index 1c16dcb56..f5ccb3866 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -63,9 +63,9 @@ function calcFocusInfo(timeInfo: TimeInfo): number { return Object.values(timeInfo).reduce((a, b) => a + b, 0) } -function calcVirtualFocusInfo(data: { [host: string]: TimeInfo }): Record { +function calcVirtualFocusInfo(data: TimeInfo[]): Record { const container: Record = {} - Object.values(data).forEach(timeInfo => Object.entries(timeInfo).forEach(([url, focusTime]) => { + data.forEach(timeInfo => Object.entries(timeInfo).forEach(([url, focusTime]) => { const virtualHosts = virtualSiteHolder.findMatched(url) virtualHosts.forEach(virtualHost => (container[virtualHost] = (container[virtualHost] || 0) + focusTime)) })) @@ -81,18 +81,19 @@ function calcVirtualFocusInfo(data: { [host: string]: TimeInfo }): Record { + const dataExclusiveWhite: [string, TimeInfo][] = Object.entries(data).filter(([host]) => whitelistHolder.notContains(host)) // 1. normal sites const normalFocusInfo: Record = {} - Object.entries(data) - .filter(([host]) => whitelistHolder.notContains(host)) - .forEach(([host, timeInfo]) => normalFocusInfo[host] = resultOf(calcFocusInfo(timeInfo), 0)) + dataExclusiveWhite.forEach(([host, timeInfo]) => normalFocusInfo[host] = resultOf(calcFocusInfo(timeInfo), 0)) // 2. virtual sites - const virtualFocusInfo: Record = calcVirtualFocusInfo(data) + const virtualFocusInfo: Record = calcVirtualFocusInfo(dataExclusiveWhite.map(arr => arr[1])) return statDatabase.accumulateBatch({ ...normalFocusInfo, ...virtualFocusInfo }, new Date()) } - async addOneTime(host: string) { - statDatabase.accumulate(host, new Date(), resultOf(0, 1)) + async addOneTime(host: string, url: string) { + const hosts: string[] = [host, ...virtualSiteHolder.findMatched(url)] + const resultSet: timer.stat.ResultSet = Object.fromEntries(hosts.map(host => [host, resultOf(0, 1)])) + statDatabase.accumulateBatch(resultSet, new Date()) } /** From 4b1ceb055e6de77cef4ab1196ae1bc46a10e991d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 23 Mar 2023 22:59:04 +0800 Subject: [PATCH 123/168] Show tooltip of the type in site management page (#196) --- .../site-manage/table/column/alias.ts | 38 ++++++------- .../site-manage/table/column/source.ts | 21 +++---- .../site-manage/table/column/type.ts | 57 +++++++++++-------- src/i18n/message/app/site-manage.ts | 51 ++++++++++++----- 4 files changed, 97 insertions(+), 70 deletions(-) diff --git a/src/app/components/site-manage/table/column/alias.ts b/src/app/components/site-manage/table/column/alias.ts index 768c0251f..9dfdf4885 100644 --- a/src/app/components/site-manage/table/column/alias.ts +++ b/src/app/components/site-manage/table/column/alias.ts @@ -25,27 +25,23 @@ function handleChange(newAlias: string, row: timer.site.SiteInfo) { } } -const _default = defineComponent({ - name: "AliasColumn", - setup() { - return () => h(ElTableColumn, { - prop: 'host', - minWidth: 100, - align: 'center', - }, { - 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' }, - () => h(ElIcon, { size: 11 }, () => h(InfoFilled)) - ) - return [label, ' ', infoTooltip] - } - }) - } +const renderTooltip = () => h(ElTooltip, + { content: tooltip, placement: 'top' }, + () => h(ElIcon, { size: 11 }, () => h(InfoFilled)) +) + +const render = () => h(ElTableColumn, { + prop: 'host', + minWidth: 100, + align: 'center', +}, { + default: ({ row }: { row: timer.site.SiteInfo }) => h(Editable, { + modelValue: row.alias, + onChange: (newAlias: string) => handleChange(newAlias, row) + }), + header: () => [label, ' ', renderTooltip()] }) +const _default = defineComponent({ render }) + export default _default \ No newline at end of file diff --git a/src/app/components/site-manage/table/column/source.ts b/src/app/components/site-manage/table/column/source.ts index 525f61ea0..0994ca1ed 100644 --- a/src/app/components/site-manage/table/column/source.ts +++ b/src/app/components/site-manage/table/column/source.ts @@ -19,18 +19,15 @@ function renderSource(source: timer.site.AliasSource) { return h(ElTag, { type, size: 'small' }, () => SOURCE_DESC[source]) } -const _default = defineComponent({ - name: "SourceColumn", - setup() { - return () => h(ElTableColumn, { - prop: 'source', - label: t(msg => msg.siteManage.column.source), - minWidth: 70, - align: 'center', - }, { - default: ({ row }: { row: timer.site.SiteInfo }) => row.source ? renderSource(row.source) : '' - }) - } +const render = () => h(ElTableColumn, { + prop: 'source', + label: t(msg => msg.siteManage.column.source), + minWidth: 70, + align: 'center', +}, { + default: ({ row }: { row: timer.site.SiteInfo }) => row.source ? renderSource(row.source) : '' }) +const _default = defineComponent({ render }) + export default _default \ No newline at end of file diff --git a/src/app/components/site-manage/table/column/type.ts b/src/app/components/site-manage/table/column/type.ts index cf5c34ee0..b65fb4596 100644 --- a/src/app/components/site-manage/table/column/type.ts +++ b/src/app/components/site-manage/table/column/type.ts @@ -1,21 +1,24 @@ import { t } from "@app/locale" -import { ElTableColumn, ElTag } from "element-plus" +import { ElIcon, ElTableColumn, ElTag, ElTooltip } from "element-plus" +import { InfoFilled } from "@element-plus/icons-vue" import { defineComponent, h } from "vue" +import { SiteManageMessage } from "@i18n/message/app/site-manage" -const label = t(msg => msg.siteManage.column.type) +type Type = keyof SiteManageMessage['type'] +const ALL_TYPES: Type[] = ['normal', 'merged', 'virtual'] -const normalType = t(msg => msg.siteManage.type.normal) -const mergedType = t(msg => msg.siteManage.type.merged) -const virtualType = t(msg => msg.siteManage.type.virtual) +const label = t(msg => msg.siteManage.column.type) function cumputeText({ merged, virtual }: timer.site.SiteInfo): string { + let type: Type = undefined if (merged) { - return mergedType + type = 'merged' } else if (virtual) { - return virtualType + type = 'virtual' } else { - return normalType + type = 'normal' } + return t(msg => msg.siteManage.type[type].name) } function computeType({ merged, virtual }: timer.site.SiteInfo): 'info' | 'success' | '' { @@ -28,21 +31,29 @@ function computeType({ merged, virtual }: timer.site.SiteInfo): 'info' | 'succes } } -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)) - }) - } +const renderTooltip = () => h(ElTooltip, { placement: 'top' }, { + content: () => ALL_TYPES + .map((type: Type) => `${t(msg => msg.siteManage.type[type].name)} - ${t(msg => msg.siteManage.type[type].info)}`) + .reduce((a, b) => { + a.length && a.push(h('br')) + a.push(b) + return a + }, []), + default: () => h(ElIcon, { size: 11 }, () => h(InfoFilled)), }) +const renderContent = (row: timer.site.SiteInfo) => h(ElTag, { + size: 'small', + type: computeType(row), +}, () => cumputeText(row)) + +const _default = defineComponent(() => () => h(ElTableColumn, { + prop: 'host', + minWidth: 60, + align: 'center', +}, { + header: () => [label, ' ', renderTooltip()], + default: ({ row }: { row: timer.site.SiteInfo }) => renderContent(row) +})) + export default _default \ No newline at end of file diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts index d2d1266f0..594b88995 100644 --- a/src/i18n/message/app/site-manage.ts +++ b/src/i18n/message/app/site-manage.ts @@ -18,11 +18,7 @@ export type SiteManageMessage = { source: string icon: string } - type: { - normal: string - merged: string - virtual: string - } + type: Record<'normal' | 'merged' | 'virtual', Record<'name' | 'info', string>> source: { user: string detected: string @@ -60,9 +56,18 @@ const _default: Messages = { icon: '网站图标', }, type: { - normal: '普通站点', - merged: '合并站点', - virtual: '自定义站点', + normal: { + name: '普通站点', + info: '按域名的维度统计', + }, + merged: { + name: '合并站点', + info: '将多个相关的域名合并统计,合并规则可以自定义' + }, + virtual: { + name: '自定义站点', + info: '统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点' + }, }, source: { user: '手动设置', @@ -103,9 +108,18 @@ const _default: Messages = { detected: '自動抓取', }, type: { - normal: '普通站點', - merged: '合併站點', - virtual: '自定義站點', + normal: { + name: '普通站點', + info: '按域名的維度統計', + }, + merged: { + name: '合併站點', + info: '將多個相關的域名合併統計,合併規則可以自定義', + }, + virtual: { + name: '自定義站點', + info: '統計 Ant Pattern 格式的任意 URL,可以在右上角新增自定義站點', + }, }, button: { add: '新增', @@ -137,9 +151,18 @@ const _default: Messages = { icon: 'Icon', }, type: { - normal: 'normal', - merged: 'merged', - virtual: 'virtual', + normal: { + name: 'normal', + info: 'statistics by domain name', + }, + merged: { + name: 'merged', + info: 'merge statistics of multiple related domain names, and the merge rules can be customized', + }, + virtual: { + name: 'virtual', + info: 'count any URL in Ant Pattern format, you can add a custom site in the upper right corner', + }, }, source: { user: 'user-maintained', From ab3f7fe93de46bc78acc675a921c0c3edf75cba0 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Mar 2023 13:32:57 +0800 Subject: [PATCH 124/168] Clean code --- src/database/stat-database/filter.ts | 129 ++++++++++++++ .../index.ts} | 158 +++--------------- 2 files changed, 148 insertions(+), 139 deletions(-) create mode 100644 src/database/stat-database/filter.ts rename src/database/{stat-database.ts => stat-database/index.ts} (57%) diff --git a/src/database/stat-database/filter.ts b/src/database/stat-database/filter.ts new file mode 100644 index 000000000..c625664ab --- /dev/null +++ b/src/database/stat-database/filter.ts @@ -0,0 +1,129 @@ +import { DATE_FORMAT } from "@db/common/constant" +import { judgeVirtualFast } from "@util/pattern" +import { formatTime } from "@util/time" +import StatDatabase, { StatCondition } from "." + +type _StatCondition = StatCondition & { + // Use exact date condition + useExactDate?: boolean + // date str + exactDateStr?: string + startDateStr?: string + endDateStr?: string + // time range + timeStart?: number + timeEnd?: number + focusStart?: number + focusEnd?: number +} + +type _FilterResult = { + host: string + date: string + value: timer.stat.Result +} + +function filterHost(host: string, condition: _StatCondition): boolean { + const paramHost = (condition.host || '').trim() + const exlcusiveVirtual = condition.exlcusiveVirtual + if (!paramHost) return true + if (!!condition.fullHost && host !== paramHost) return false + if (!condition.fullHost && !host.includes(paramHost)) return false + if (exlcusiveVirtual && judgeVirtualFast(host)) return false + return true +} + +function filterDate(date: string, condition: _StatCondition): boolean { + if (condition.useExactDate) { + if (condition.exactDateStr !== date) return false + } else { + const { startDateStr, endDateStr } = condition + if (startDateStr && startDateStr > date) return false + if (endDateStr && endDateStr < date) return false + } + return true +} + +function filterNumberRange(val: number, range: number[]): boolean { + const start = range[0] + const end = range[1] + if (start !== null && start !== undefined && start > val) return false + if (end !== null && end !== undefined && end < val) return false + return true +} + +/** + * Filter by query parameters + * + * @param date date of item + * @param host host of item + * @param val val of item + * @param condition query parameters + * @return true if valid, or false + */ +function filterByCond(result: _FilterResult, condition: _StatCondition): boolean { + const { host, date, value } = result + const { focus, time } = value + const { timeStart, timeEnd, focusStart, focusEnd } = condition + + return filterHost(host, condition) + && filterDate(date, condition) + && filterNumberRange(time, [timeStart, timeEnd]) + && filterNumberRange(focus, [focusStart, focusEnd]) +} + + +function processDateCondition(cond: _StatCondition, paramDate: Date | Date[]) { + if (!paramDate) return + + if (paramDate instanceof Date) { + cond.useExactDate = true + cond.exactDateStr = formatTime(paramDate as Date, DATE_FORMAT) + } else { + let startDate: Date = undefined + let endDate: Date = undefined + const dateArr = paramDate as Date[] + dateArr && dateArr.length >= 2 && (endDate = dateArr[1]) + dateArr && dateArr.length >= 1 && (startDate = dateArr[0]) + cond.useExactDate = false + startDate && (cond.startDateStr = formatTime(startDate, DATE_FORMAT)) + endDate && (cond.endDateStr = formatTime(endDate, DATE_FORMAT)) + } +} + +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: _StatCondition, paramFocus: number[]) { + if (!paramFocus) return + paramFocus.length >= 2 && (cond.focusEnd = paramFocus[1]) + paramFocus.length >= 1 && (cond.focusStart = paramFocus[0]) +} + + +function processCondition(condition: StatCondition): _StatCondition { + const result: _StatCondition = { ...condition } + processDateCondition(result, condition.date) + processParamTimeCondition(result, condition.timeRange) + processParamFocusCondition(result, condition.focusRange) + return result +} + +/** + * Filter by query parameters + */ +export async function filter(this: StatDatabase, condition?: StatCondition): Promise<_FilterResult[]> { + condition = condition || {} + const cond = processCondition(condition) + const items = await this.refresh() + return Object.entries(items).map( + ([key, value]) => { + const date = key.substring(0, 8) + const host = key.substring(8) + return { date, host, value: value as timer.stat.Result } + } + ).filter(item => filterByCond(item, cond)) +} \ No newline at end of file diff --git a/src/database/stat-database.ts b/src/database/stat-database/index.ts similarity index 57% rename from src/database/stat-database.ts rename to src/database/stat-database/index.ts index 0cdc99ccd..23b64cf51 100644 --- a/src/database/stat-database.ts +++ b/src/database/stat-database/index.ts @@ -5,12 +5,13 @@ * https://opensource.org/licenses/MIT */ -import { log } from "../common/logger" +import { log } from "../../common/logger" import { formatTime } from "@util/time" -import BaseDatabase from "./common/base-database" -import { DATE_FORMAT, REMAIN_WORD_PREFIX } from "./common/constant" +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" +import { filter } from "./filter" export type StatCondition = { /** @@ -48,58 +49,6 @@ export type StatCondition = { exlcusiveVirtual?: boolean } -type _StatCondition = StatCondition & { - // Use exact date condition - useExactDate?: boolean - // date str - exactDateStr?: string - startDateStr?: string - endDateStr?: string - // time range - timeStart?: number - timeEnd?: number - focusStart?: number - focusEnd?: number -} - -function processDateCondition(cond: _StatCondition, paramDate: Date | Date[]) { - if (!paramDate) return - - if (paramDate instanceof Date) { - cond.useExactDate = true - cond.exactDateStr = formatTime(paramDate as Date, DATE_FORMAT) - } else { - let startDate: Date = undefined - let endDate: Date = undefined - const dateArr = paramDate as Date[] - dateArr && dateArr.length >= 2 && (endDate = dateArr[1]) - dateArr && dateArr.length >= 1 && (startDate = dateArr[0]) - cond.useExactDate = false - startDate && (cond.startDateStr = formatTime(startDate, DATE_FORMAT)) - endDate && (cond.endDateStr = formatTime(endDate, DATE_FORMAT)) - } -} - -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: _StatCondition, paramFocus: number[]) { - if (!paramFocus) return - paramFocus.length >= 2 && (cond.focusEnd = paramFocus[1]) - paramFocus.length >= 1 && (cond.focusStart = paramFocus[0]) -} - -function processCondition(condition: StatCondition): _StatCondition { - const result: _StatCondition = { ...condition } - processDateCondition(result, condition.date) - processParamTimeCondition(result, condition.timeRange) - processParamFocusCondition(result, condition.focusRange) - return result -} - function mergeMigration(exist: timer.stat.Result | undefined, another: any) { exist = exist || createZeroResult() return mergeResult(exist, { focus: another.focus || 0, time: another.time || 0 }) @@ -181,6 +130,8 @@ class StatDatabase extends BaseDatabase { return afterUpdated } + filter = filter + /** * Select * @@ -188,71 +139,24 @@ class StatDatabase extends BaseDatabase { */ async select(condition?: StatCondition): Promise { log("select:{condition}", condition) - condition = condition || {} - const _cond: _StatCondition = processCondition(condition) - const items = await this.refresh() - let result: timer.stat.Row[] = [] - - for (let key in items) { - const date = key.substring(0, 8) - const host = key.substring(8) - 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: [], virtual: judgeVirtualFast(host) }) - } - } - - log('Result of select: ', result) - return result - } - - private filterHost(host: string, condition: _StatCondition): boolean { - const paramHost = (condition.host || '').trim() - const exlcusiveVirtual = condition.exlcusiveVirtual - if (!paramHost) return true - if (!!condition.fullHost && host !== paramHost) return false - if (!condition.fullHost && !host.includes(paramHost)) return false - if (exlcusiveVirtual && judgeVirtualFast(host)) return false - return true - } - - private filterDate(date: string, condition: _StatCondition): boolean { - if (condition.useExactDate) { - if (condition.exactDateStr !== date) return false - } else { - const { startDateStr, endDateStr } = condition - if (startDateStr && startDateStr > date) return false - if (endDateStr && endDateStr < date) return false - } - return true - } - - private filterNumberRange(val: number, range: number[]): boolean { - const start = range[0] - const end = range[1] - if (start !== null && start !== undefined && start > val) return false - if (end !== null && end !== undefined && end < val) return false - return true + const filterResults = await this.filter(condition) + return filterResults.map(({ date, host, value }) => { + const { focus, time } = value + return { date, host, focus, time, mergedHosts: [], virtual: judgeVirtualFast(host) } + }) } /** - * Filter by query parameters + * Count by condition * - * @param date date of item - * @param host host of item - * @param val val of item - * @param condition query parameters - * @return true if valid, or false + * @param condition + * @returns count + * @since 1.0.2 */ - private filterBefore(date: string, host: string, val: timer.stat.Result, condition: _StatCondition): boolean { - const { focus, time } = val - const { timeStart, timeEnd, focusStart, focusEnd } = condition - - return this.filterHost(host, condition) - && this.filterDate(date, condition) - && this.filterNumberRange(time, [timeStart, timeEnd]) - && this.filterNumberRange(focus, [focusStart, focusEnd]) + async count(condition: StatCondition): Promise { + log("select:{condition}", condition) + const filterResults = await this.filter(condition) + return filterResults.length || 0 } /** @@ -331,30 +235,6 @@ class StatDatabase extends BaseDatabase { return this.deleteByUrlBetween(host) } - /** - * Count by condition - * - * @param condition - * @returns count - * @since 1.0.2 - */ - async count(condition: StatCondition): Promise { - condition = condition || {} - const _cond: _StatCondition = processCondition(condition) - const items = await this.refresh() - let count = 0 - - for (let key in items) { - const date = key.substring(0, 8) - const host = key.substring(8) - const val: timer.stat.Result = items[key] - if (this.filterBefore(date, host, val, _cond)) { - count++ - } - } - return count - } - async importData(data: any): Promise { if (typeof data !== "object") return const items = await this.storage.get() From 93761915ebb0c35338946fa5171ce08bd84cd6ad Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Mar 2023 13:44:20 +0800 Subject: [PATCH 125/168] Not allow to delay by default (#198) --- src/app/components/limit/index.ts | 103 +++++++++++------------ src/app/components/limit/modify/index.ts | 2 +- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/src/app/components/limit/index.ts b/src/app/components/limit/index.ts index bb7f7dc9b..80deb65f1 100644 --- a/src/app/components/limit/index.ts +++ b/src/app/components/limit/index.ts @@ -17,64 +17,61 @@ import { t } from "@app/locale" import { ElMessage } from "element-plus" import { handleWindowVisibleChange } from "@util/window" -const _default = defineComponent({ - name: "Limit", - setup() { - const url: Ref = ref('') - const onlyEnabled: Ref = ref(false) - const data: Ref = ref([]) - // Init and query - const queryData = async () => { - const list = await limitService.select({ filterDisabled: onlyEnabled.value, url: url.value || '' }) - data.value = list - } - queryData() - // Query data if the window become visible - handleWindowVisibleChange(queryData) - // Init with url parameter - const urlParam = useRoute().query['url'] as string - useRouter().replace({ query: {} }) - urlParam && (url.value = decodeURIComponent(urlParam)) +const _default = defineComponent(() => { + const url: Ref = ref('') + const onlyEnabled: Ref = ref(false) + const data: Ref = ref([]) + // Init and query + const queryData = async () => { + const list = await limitService.select({ filterDisabled: onlyEnabled.value, url: url.value || '' }) + data.value = list + } + queryData() + // Query data if the window become visible + handleWindowVisibleChange(queryData) + // Init with url parameter + const urlParam = useRoute().query['url'] as string + useRouter().replace({ query: {} }) + urlParam && (url.value = decodeURIComponent(urlParam)) - const modify: Ref = ref() - const test: Ref = ref() + const modify: Ref = ref() + const test: Ref = ref() - return () => h(ContentContainer, {}, { - filter: () => h(LimitFilter, { - url: url.value, - onlyEnabled: onlyEnabled.value, - onChange(option: LimitFilterOption) { - url.value = option.url - onlyEnabled.value = option.onlyEnabled + return () => h(ContentContainer, {}, { + filter: () => h(LimitFilter, { + url: url.value, + onlyEnabled: onlyEnabled.value, + onChange(option: LimitFilterOption) { + url.value = option.url + onlyEnabled.value = option.onlyEnabled + queryData() + }, + onCreate: () => modify.value?.create?.(), + onTest: () => test.value?.show?.(), + }), + content: () => [ + h(LimitTable, { + data: data.value, + onDelayChange: (row: timer.limit.Item) => limitService.updateDelay(row), + onEnabledChange: (row: timer.limit.Item) => limitService.updateEnabled(row), + async onDelete(row: timer.limit.Item) { + await limitService.remove(row) + ElMessage.success(t(msg => msg.limit.message.deleted)) queryData() }, - onCreate: () => modify.value?.create?.(), - onTest: () => test.value?.show?.(), + async onModify(row: timer.limit.Item) { + modify.value?.modify?.(row) + } }), - content: () => [ - h(LimitTable, { - data: data.value, - onDelayChange: (row: timer.limit.Item) => limitService.updateDelay(row), - onEnabledChange: (row: timer.limit.Item) => limitService.updateEnabled(row), - async onDelete(row: timer.limit.Item) { - await limitService.remove(row) - ElMessage.success(t(msg => msg.limit.message.deleted)) - queryData() - }, - async onModify(row: timer.limit.Item) { - modify.value?.modify?.(row) - } - }), - h(LimitModify, { - ref: modify, - onSave: queryData - }), - h(LimitTest, { - ref: test - }), - ] - }) - } + h(LimitModify, { + ref: modify, + onSave: queryData + }), + h(LimitTest, { + ref: test + }), + ] + }) }) export default _default diff --git a/src/app/components/limit/modify/index.ts b/src/app/components/limit/modify/index.ts index 78994723b..9f5e47b23 100644 --- a/src/app/components/limit/modify/index.ts +++ b/src/app/components/limit/modify/index.ts @@ -61,7 +61,7 @@ const _default = defineComponent({ ElMessage.warning(noTimeError) return } - const toSave: timer.limit.Rule = { cond: condition, time: timeLimit, enabled: true, allowDelay: true } + const toSave: timer.limit.Rule = { cond: condition, time: timeLimit, enabled: true, allowDelay: false } if (mode.value === 'modify' && modifyingItem) { toSave.enabled = modifyingItem.enabled toSave.allowDelay = modifyingItem.allowDelay From 8351ad5331e8a289620e5b02e93b3ee4c4adbea8 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Mar 2023 15:00:45 +0800 Subject: [PATCH 126/168] Display the time last backup (#200) --- .../option/components/backup/footer.ts | 73 +++++++++++++++++++ .../option/components/backup/index.ts | 22 +----- src/common/backup/processor.ts | 7 +- src/i18n/message/app/option.ts | 5 ++ src/i18n/message/common/calendar.ts | 5 ++ 5 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 src/app/components/option/components/backup/footer.ts diff --git a/src/app/components/option/components/backup/footer.ts b/src/app/components/option/components/backup/footer.ts new file mode 100644 index 000000000..24b300a10 --- /dev/null +++ b/src/app/components/option/components/backup/footer.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import { PropType, Ref, watch } from "vue" + +import { t } from "@app/locale" +import { UploadFilled } from "@element-plus/icons-vue" +import { ElButton, ElLoading, ElMessage, ElText } from "element-plus" +import { defineComponent, h, ref } from "vue" +import metaService from "@service/meta-service" +import processor from "@src/common/backup/processor" +import { formatTime } from "@util/time" + +async function handleBackup(lastTime: Ref) { + const loading = ElLoading.service({ + text: "Doing backup...." + }) + const result = await processor.syncData() + loading.close() + if (result.success) { + ElMessage.success('Successfully!') + lastTime.value = result.data || Date.now() + } else { + ElMessage.error(result.errorMsg || 'Unknown error') + } +} + +const TIME_FORMAT = t(msg => msg.calendar.timeFormat) + +const _default = defineComponent({ + props: { + type: { + type: String as PropType, + required: false, + } + }, + setup(props) { + const lastTime: Ref = ref(undefined) + + const queryLastTime = async () => { + const backInfo = await metaService.getLastBackUp(props.type) + lastTime.value = backInfo?.ts + } + + queryLastTime() + watch(() => props.type, queryLastTime) + + return () => { + const children = [ + h(ElButton, { + type: 'primary', + icon: UploadFilled, + onClick: () => handleBackup(lastTime) + }, () => t(msg => msg.option.backup.operation)) + ] + const lastTimeVal = lastTime.value + if (lastTimeVal) { + const tips = t(msg => msg.option.backup.lastTimeTip, { + lastTime: formatTime(lastTimeVal, TIME_FORMAT) + }) + const tipText = h(ElText, { style: { marginLeft: '20px' } }, () => tips) + children.push(tipText) + } + lastTime.value && children.push() + return h('div', {}, children) + } + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index 0cbd73441..66c7c458d 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -14,8 +14,8 @@ import { defaultBackup } from "@util/constant/option" import { ElInput, ElOption, ElSelect, ElDivider, ElAlert, ElButton, ElMessage, ElLoading } from "element-plus" import { defineComponent, ref, h } from "vue" import { renderOptionItem, tooltip } from "../../common" -import { UploadFilled } from "@element-plus/icons-vue" import BackUpAutoInput from "./auto-input" +import Footer from "./footer" const ALL_TYPES: timer.backup.Type[] = [ 'none', @@ -114,19 +114,6 @@ const _default = defineComponent({ } } - async function handleBackup() { - const loading = ElLoading.service({ - text: "Doing backup...." - }) - const result = await processor.syncData() - loading.close() - if (result.success) { - ElMessage.success('Successfully!') - } else { - ElMessage.error(result.errorMsg || 'Unknown error') - } - } - ctx.expose({ async reset() { // Only reset type and auto flag @@ -151,6 +138,7 @@ const _default = defineComponent({ t(msg => msg.option.backup.meta[DEFAULT.backupType].label) ) ] + console.log(type.value) type.value !== 'none' && nodes.push( h(ElDivider), renderOptionItem({ @@ -178,11 +166,7 @@ const _default = defineComponent({ msg => msg.backup.client ), h(ElDivider), - h(ElButton, { - type: 'primary', - icon: UploadFilled, - onClick: handleBackup - }, () => t(msg => msg.option.backup.operation)), + h(Footer, { type: type.value }), ) return h('div', nodes) } diff --git a/src/common/backup/processor.ts b/src/common/backup/processor.ts index 07b4c0490..4a708a2bd 100644 --- a/src/common/backup/processor.ts +++ b/src/common/backup/processor.ts @@ -145,7 +145,7 @@ class Processor { } } - async syncData(): Promise> { + async syncData(): Promise> { const option = (await optionService.getAllOption()) as timer.option.BackupOption const auth = option?.backupAuths?.[option.backupType || 'none'] @@ -173,8 +173,9 @@ class Processor { clients.push(client) await coordinator.updateClients(context, clients) // Update time - metaService.updateBackUpTime(type, Date.now()) - return success() + const now = Date.now() + metaService.updateBackUpTime(type, now) + return success(now) } async query(type: timer.backup.Type, auth: string, start: Date, end: Date): Promise { diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index b71ec76ba..dd34d377e 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -73,6 +73,7 @@ export type OptionMessage = { alert: string test: string operation: string + lastTimeTip: string auto: { label: string interval: string @@ -167,6 +168,7 @@ const _default: Messages = { }, alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', test: '测试', + lastTimeTip: '上次备份时间: {lastTime}', operation: '备份数据', auto: { label: '是否开启自动备份', @@ -253,6 +255,7 @@ const _default: Messages = { alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', test: '測試', operation: '備份數據', + lastTimeTip: '上次備份時間: {lastTime}', auto: { label: '是否開啟自動備份', interval: '每 {input} 分鐘備份一次', @@ -338,6 +341,7 @@ const _default: Messages = { alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', test: 'Test', operation: 'Backup', + lastTimeTip: 'Last backup time: {lastTime}', auto: { label: 'Whether to enable automatic backup', interval: 'and run every {input} minutes', @@ -423,6 +427,7 @@ const _default: Messages = { alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', test: 'テスト', operation: 'バックアップ', + lastTimeTip: '前回のバックアップ時間: {lastTime}', auto: { label: '自動バックアップを有効にするかどうか', interval: ' {input} 分ごとに実行', diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index 4313380cb..46de6d195 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -9,6 +9,7 @@ export type CalendarMessage = { weekDays: string months: string dateFormat: string + timeFormat: string } const _default: Messages = { @@ -16,21 +17,25 @@ const _default: Messages = { weekDays: '星期一|星期二|星期三|星期四|星期五|星期六|星期天', months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', dateFormat: '{y}/{m}/{d}', + timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', }, zh_TW: { weekDays: '禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天', months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', dateFormat: '{y}/{m}/{d}', + timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', }, en: { weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', dateFormat: '{m}/{d}/{y}', + timeFormat: '{m}/{d}/{y} {h}:{i}:{s}', }, ja: { weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', dateFormat: '{y}/{m}/{d}', + timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', }, } From 65d13f69f054b518a75640e65411ac6d4391609c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 26 Mar 2023 15:12:12 +0800 Subject: [PATCH 127/168] v1.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5809719fc..bc3ed82a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.6.0", + "version": "1.6.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From 9a1570e537d343ce91e4d563c93a3bc40e537a05 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 28 Mar 2023 00:26:50 +0800 Subject: [PATCH 128/168] Fix bugs of translation --- script/crowdin/sync-translation.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index 40de1ae9d..08b9ebf3d 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -6,6 +6,7 @@ import { SourceFilesModel } from "@crowdin/crowdin-api-client" import { groupBy } from "@util/array" async function processDirMessage(client: CrowdinClient, file: SourceFilesModel.File, message: ItemSet, lang: CrowdinLanguage): Promise { + console.log(`Start to process dir message: fileName=${file.name}, lang=${lang}`) const strings = await client.listStringsByFile(file.id) const stringMap = groupBy(strings, s => s.identifier, l => l[0]) for (const [identifier, text] of Object.entries(message)) { @@ -42,10 +43,11 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo console.log(`find ${files.length} files of ${dir}`) const fileMap = groupBy(files, f => f.name, l => l[0]) for (const [tsFilename, message] of Object.entries(messages)) { + console.log(`Start to sync translations of ${dir}/${tsFilename}`) if (isIgnored(dir, tsFilename)) { + console.log("Ignored file: " + tsFilename) continue } - console.log(`Start to sync translations of ${tsFilename}`) const filename = tsFilename.replace('.ts', '.json') const crowdinFile = fileMap[filename] if (!crowdinFile) { @@ -56,7 +58,7 @@ 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 + continue } const strings = transMsg(message[locale]) const crwodinLang = crowdinLangOf(locale) @@ -69,6 +71,9 @@ async function main() { const client = getClientFromEnv() const branch = await checkMainBranch(client) + for (let i = 0; i < ALL_DIRS.length; i++) { + + } for (const dir of ALL_DIRS) { await processDir(client, dir, branch) } From 00dffa9a1ffb2b875d2fa7df10d86732e24d74bc Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 28 Mar 2023 21:51:01 +0800 Subject: [PATCH 129/168] Download translations --- src/i18n/message/app/site-manage.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts index 594b88995..51488b5ab 100644 --- a/src/i18n/message/app/site-manage.ts +++ b/src/i18n/message/app/site-manage.ts @@ -62,11 +62,11 @@ const _default: Messages = { }, merged: { name: '合并站点', - info: '将多个相关的域名合并统计,合并规则可以自定义' + info: '将多个相关的域名合并统计,合并规则可以自定义', }, virtual: { name: '自定义站点', - info: '统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点' + info: '统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点', }, }, source: { @@ -195,6 +195,8 @@ const _default: Messages = { alias: 'サイト名', aliasInfo: 'サイト名はレコードページとポップアップページに表示されます', source: 'ソース', + type: 'サイト種別', + icon: 'Icon', }, source: { user: '手动输入', @@ -214,6 +216,21 @@ const _default: Messages = { saved: '保存しました', existedTag: '既存', mergedTag: '合并', + virtualTag: 'バーチャル', + }, + type: { + normal: { + name: '普通', + info: 'ドメイン名による統計', + }, + merged: { + name: '合并', + info: '複数の関連するドメイン名のマージ統計をカスタマイズできます', + }, + virtual: { + name: 'バーチャル', + info: 'Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます', + }, }, }, } From f133fd6a5c7a981cd618b200f67f36c12e678c51 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 9 Apr 2023 00:24:39 +0800 Subject: [PATCH 130/168] Site analysis(#201) --- .../analysis/components/common/indicator.ts | 54 ++++ .../analysis/components/common/row-card.ts | 25 ++ .../components/analysis/components/filter.ts | 131 +++++++++ .../analysis/components/summary/index.ts | 96 +++++++ .../analysis/components/summary/site.ts | 44 +++ .../analysis/components/summary/summary.sass | 22 ++ .../components/trend/dimension/chart.ts | 37 +++ .../components/trend/dimension/index.ts | 70 +++++ .../components/trend/dimension/wrapper.ts | 106 ++++++++ .../analysis/components/trend/filter.ts | 60 +++++ .../analysis/components/trend/index.ts | 227 ++++++++++++++++ .../analysis/components/trend/style.sass | 43 +++ .../analysis/components/trend/total.ts | 77 ++++++ src/app/components/analysis/index.ts | 81 ++++++ src/app/components/analysis/style.sass | 39 +++ src/app/components/analysis/util.ts | 54 ++++ src/app/components/common/content-card.ts | 16 ++ .../components/common/content-container.ts | 33 +-- src/app/components/habit/component/filter.ts | 4 - .../option/components/backup/index.ts | 1 - src/app/components/report/file-export.ts | 2 +- .../components/report/table/columns/date.ts | 4 +- .../components/report/table/columns/focus.ts | 2 +- .../table/columns/operation-delete-button.ts | 8 +- .../report/table/columns/operation.ts | 40 +-- .../trend/components/chart/index.ts | 33 --- .../trend/components/chart/wrapper.ts | 251 ------------------ src/app/components/trend/components/common.ts | 21 -- src/app/components/trend/components/filter.ts | 172 ------------ src/app/components/trend/index.ts | 91 ------- src/app/components/trend/trend.d.ts | 18 -- src/app/layout/menu.ts | 5 +- src/app/router/constants.ts | 2 +- src/app/router/index.ts | 6 +- src/app/styles/index.sass | 4 +- .../report/formatter.ts => util/time.ts} | 8 +- src/i18n/message/app/analysis.ts | 177 ++++++++++++ src/i18n/message/app/index.ts | 12 +- src/i18n/message/app/menu.ts | 10 +- src/i18n/message/app/trend.ts | 122 --------- src/i18n/message/common/item.ts | 10 +- src/service/stat-service/index.ts | 9 +- src/util/time.ts | 20 ++ 43 files changed, 1456 insertions(+), 791 deletions(-) create mode 100644 src/app/components/analysis/components/common/indicator.ts create mode 100644 src/app/components/analysis/components/common/row-card.ts create mode 100644 src/app/components/analysis/components/filter.ts create mode 100644 src/app/components/analysis/components/summary/index.ts create mode 100644 src/app/components/analysis/components/summary/site.ts create mode 100644 src/app/components/analysis/components/summary/summary.sass create mode 100644 src/app/components/analysis/components/trend/dimension/chart.ts create mode 100644 src/app/components/analysis/components/trend/dimension/index.ts create mode 100644 src/app/components/analysis/components/trend/dimension/wrapper.ts create mode 100644 src/app/components/analysis/components/trend/filter.ts create mode 100644 src/app/components/analysis/components/trend/index.ts create mode 100644 src/app/components/analysis/components/trend/style.sass create mode 100644 src/app/components/analysis/components/trend/total.ts create mode 100644 src/app/components/analysis/index.ts create mode 100644 src/app/components/analysis/style.sass create mode 100644 src/app/components/analysis/util.ts create mode 100644 src/app/components/common/content-card.ts delete mode 100644 src/app/components/trend/components/chart/index.ts delete mode 100644 src/app/components/trend/components/chart/wrapper.ts delete mode 100644 src/app/components/trend/components/common.ts delete mode 100644 src/app/components/trend/components/filter.ts delete mode 100644 src/app/components/trend/index.ts delete mode 100644 src/app/components/trend/trend.d.ts rename src/app/{components/report/formatter.ts => util/time.ts} (90%) create mode 100644 src/i18n/message/app/analysis.ts delete mode 100644 src/i18n/message/app/trend.ts diff --git a/src/app/components/analysis/components/common/indicator.ts b/src/app/components/analysis/components/common/indicator.ts new file mode 100644 index 000000000..f40fc10fb --- /dev/null +++ b/src/app/components/analysis/components/common/indicator.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@app/locale" +import type { PropType, VNode } from "vue" + +import { tN } from "@app/locale" +import { defineComponent, h } from "vue" + +export type IndicatorProps = { + mainName: string + mainValue: string + subTips?: I18nKey + subValue?: string +} + +function renderChildren(props: IndicatorProps): VNode[] { + const { mainName, subTips, mainValue, subValue } = props + const children = [ + h('div', { class: 'indicator-name' }, mainName), + h('div', { class: 'indicator-value' }, mainValue || '-'), + ] + const subTipsLine = [] + if (subTips || subValue) { + const subValueSpan = h('span', { class: 'indicator-sub-value' }, subValue || '-') + if (subTips) { + subTipsLine.push(...tN(subTips, { value: subValueSpan })) + } else { + subTipsLine.push(subValueSpan) + } + } else { + subTipsLine.push('') + } + children.push(h('div', { class: 'indicator-sub-tip' }, subTipsLine)) + return children +} + +const _default = defineComponent({ + props: { + mainName: String, + mainValue: String, + subTips: Function as PropType, + subValue: String, + }, + setup(props) { + return () => h('div', { class: 'analysis-indicator-container' }, renderChildren(props)) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/common/row-card.ts b/src/app/components/analysis/components/common/row-card.ts new file mode 100644 index 000000000..d8617f872 --- /dev/null +++ b/src/app/components/analysis/components/common/row-card.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElCard } from "element-plus" +import { defineComponent, h } from "vue" + +const _default = defineComponent({ + props: { + title: String + }, + setup(props, ctx) { + const slots = ctx.slots + const { default: default_ } = slots + return () => { + const title = h('div', { class: 'analysis-row-title' }, props.title) + return h(ElCard, { class: 'analysis-row-card' }, () => [title, h(default_, { class: 'analysis-row-body' })]) + } + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/filter.ts b/src/app/components/analysis/components/filter.ts new file mode 100644 index 000000000..3d2db2453 --- /dev/null +++ b/src/app/components/analysis/components/filter.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { Ref, PropType, VNode } from "vue" + +import { ElOption, ElSelect, ElTag } from "element-plus" +import { ref, h, defineComponent } from "vue" +import statService, { HostSet } from "@service/stat-service" +import { t } from "@app/locale" +import SelectFilterItem from "@app/components/common/select-filter-item" +import { labelOfHostInfo } from "../util" + +async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { + if (!queryStr) { + trendDomainOptions.value = [] + return + } + searching.value = true + const domains: HostSet = await statService.listHosts(queryStr) + const options: timer.site.SiteKey[] = [] + 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 +} + +const HOST_PLACEHOLDER = t(msg => msg.analysis.common.hostPlaceholder) + +const TIME_FORMAT_LABELS: { [key in timer.app.TimeFormat]: string } = { + default: t(msg => msg.timeFormat.default), + second: t(msg => msg.timeFormat.second), + minute: t(msg => msg.timeFormat.minute), + hour: t(msg => msg.timeFormat.hour) +} + +function keyOfHostInfo(option: timer.site.SiteKey): string { + const { merged, virtual, host } = option + let prefix = '_' + merged && (prefix = 'm') + virtual && (prefix = 'v') + return `${prefix}${host || ''}` +} + +function hostInfoOfKey(key: string): timer.site.SiteKey { + if (!key?.length) return undefined + const prefix = key.charAt(0) + return { host: key.substring(1), merged: prefix === 'm', virtual: prefix === 'v' } +} + +const MERGED_TAG_TXT = t(msg => msg.analysis.common.merged) +const VIRTUAL_TAG_TXT = t(msg => msg.analysis.common.virtual) +function renderHostLabel(hostInfo: timer.site.SiteKey): VNode[] { + const result = [ + h('span', {}, hostInfo.host) + ] + hostInfo.merged && result.push( + h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) + ) + hostInfo.virtual && result.push( + h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT) + ) + return result +} + +const _default = defineComponent({ + name: "TrendFilter", + props: { + site: Object as PropType, + timeFormat: String as PropType + }, + emits: { + siteChange: (_site: timer.site.SiteKey) => true, + timeFormatChange: (_format: timer.app.TimeFormat) => true, + }, + setup(props, ctx) { + const domainKey: Ref = ref('') + const trendSearching: Ref = ref(false) + const trendDomainOptions: Ref = ref([]) + const defaultSite: timer.site.SiteKey = props.site + const timeFormat: Ref = ref(props.timeFormat) + if (defaultSite) { + domainKey.value = keyOfHostInfo(defaultSite) + trendDomainOptions.value.push(defaultSite) + } + + function handleSiteChange() { + const siteInfo: timer.site.SiteInfo = hostInfoOfKey(domainKey.value) + ctx.emit('siteChange', siteInfo) + } + + return () => [ + h(ElSelect, { + placeholder: HOST_PLACEHOLDER, + class: 'filter-item', + modelValue: domainKey.value, + filterable: true, + remote: true, + loading: trendSearching.value, + clearable: true, + remoteMethod: (query: string) => handleRemoteSearch(query, trendDomainOptions, trendSearching), + onChange: (key: string) => { + domainKey.value = key + handleSiteChange() + }, + onClear: () => { + domainKey.value = undefined + handleSiteChange() + } + }, () => (trendDomainOptions.value || [])?.map( + hostInfo => h(ElOption, { + value: keyOfHostInfo(hostInfo), + label: labelOfHostInfo(hostInfo), + }, () => renderHostLabel(hostInfo)) + )), + h(SelectFilterItem, { + historyName: 'timeFormat', + defaultValue: timeFormat.value, + options: TIME_FORMAT_LABELS, + onSelect: (newVal: timer.app.TimeFormat) => ctx.emit('timeFormatChange', timeFormat.value = newVal) + }) + ] + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/summary/index.ts b/src/app/components/analysis/components/summary/index.ts new file mode 100644 index 000000000..6ad21f20c --- /dev/null +++ b/src/app/components/analysis/components/summary/index.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import type { PropType, Ref, VNode } from "vue" + +import { defineComponent, h, ref, watch } from "vue" +import siteService from "@service/site-service" +import Site from "./site" +import RowCard from "../common/row-card" +import Indicator from "../common/indicator" +import "./summary.sass" +import { ElCol, ElRow } from "element-plus" +import { t } from "@app/locale" +import { cvt2LocaleTime, periodFormatter } from "@app/util/time" + +type Summary = { + focus: number + visit: number + day: number + firstDay?: string +} + +function computeSummary(site: timer.site.SiteKey, rows: timer.stat.Row[]): Summary { + if (!site) return undefined + + const summary: Summary = { focus: 0, visit: 0, day: 0 } + summary.firstDay = rows?.[0]?.date + rows.forEach(({ focus, time: visit }) => { + summary.focus += focus + summary.visit += visit + focus && (summary.day += 1) + }) + return summary +} + +const DAYS_LABEL = t(msg => msg.analysis.summary.day) +const FOCUS_LABEL = t(msg => msg.analysis.common.focusTotal) +const VISIT_LABEL = t(msg => msg.analysis.common.visitTotal) + +function renderContent(siteInfo: timer.site.SiteInfo, summary: Summary, timeFormat: timer.app.TimeFormat): VNode { + const { day, firstDay, focus, visit } = summary || {} + return h(ElRow, { class: "analysis-summary-container" }, () => [ + h(ElCol, { span: 6 }, () => h(Site, { site: siteInfo })), + h(ElCol, { span: 6 }, () => h(Indicator, { + mainName: DAYS_LABEL, + mainValue: day?.toString() || '-', + subTips: msg => msg.analysis.summary.firstDay, + subValue: firstDay ? `@${cvt2LocaleTime(firstDay)}` : '' + })), + h(ElCol, { span: 6 }, () => h(Indicator, { + mainName: FOCUS_LABEL, + mainValue: focus === undefined ? '-' : periodFormatter(focus, timeFormat, false), + })), + h(ElCol, { span: 6 }, () => h(Indicator, { + mainName: VISIT_LABEL, + mainValue: visit?.toString() || '-', + })), + ]) +} + +const _default = defineComponent({ + props: { + site: Object as PropType, + timeFormat: String as PropType, + rows: Array as PropType, + }, + setup(props) { + const siteInfo: Ref = ref() + const timeFormat: Ref = ref(props.timeFormat) + const summaryInfo: Ref = ref(computeSummary(props.site, props.rows)) + + const querySiteInfo = async () => { + const siteKey = props.site + if (!siteKey) { + siteInfo.value = undefined + } else { + siteInfo.value = (await siteService.get(siteKey)) || siteKey + } + } + + watch(() => props.timeFormat, () => timeFormat.value = props.timeFormat) + watch(() => props.site, querySiteInfo) + watch(() => props.rows, () => summaryInfo.value = computeSummary(props.site, props.rows)) + + querySiteInfo() + + return () => h(RowCard, { + title: t(msg => msg.analysis.summary.title) + }, () => renderContent(siteInfo.value, summaryInfo.value, timeFormat.value)) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/summary/site.ts b/src/app/components/analysis/components/summary/site.ts new file mode 100644 index 000000000..86fd3c8c4 --- /dev/null +++ b/src/app/components/analysis/components/summary/site.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { PropType, defineComponent, h } from "vue" +import { labelOfHostInfo } from "../../util" +import { t } from "@app/locale" + +const renderIcon = (iconUrl: string) => h('img', { src: iconUrl, width: 24, height: 24 }) +const renderTitle = (title: string) => h('h1', { class: 'site-alias' }, title) +const renderSubtitle = (subtitle: string) => h('p', { class: 'site-host' }, subtitle) + +const EMPTY_DESC = t(msg => msg.analysis.common.emptyDesc) + +function renderChildren(site: timer.site.SiteInfo) { + if (!site) { + return renderTitle(EMPTY_DESC) + } + const result = [] + + const { iconUrl, alias } = site + const label = labelOfHostInfo(site) + const title: string = alias ? alias : label + const subtitle: string = alias ? label : undefined + + iconUrl && result.push(renderIcon(iconUrl)) + result.push(renderTitle(title)) + subtitle && result.push(renderSubtitle(subtitle)) + return result +} + +const _default = defineComponent({ + props: { + site: Object as PropType, + }, + setup(props) { + return () => h('div', { class: 'site-container' }, renderChildren(props.site)) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/summary/summary.sass b/src/app/components/analysis/components/summary/summary.sass new file mode 100644 index 000000000..238b3a0b3 --- /dev/null +++ b/src/app/components/analysis/components/summary/summary.sass @@ -0,0 +1,22 @@ + +.analysis-summary-container + height: 140px + >.el-col:not(:first-child) + border-left: 1px var(--el-border-color) var(--el-border-style) + .site-container + position: relative + top: 50% + transform: translateY(-50%) + text-align: center + padding: 0 25px + .site-alias + font-size: 26px + margin-block-start: 0.2em + margin-block-end: 0.5em + .site-host + font-size: 14px + color: var(--el-text-color-secondary) + .site-host,.site-alias + white-space: nowrap + overflow: hidden + text-overflow: ellipsis diff --git a/src/app/components/analysis/components/trend/dimension/chart.ts b/src/app/components/analysis/components/trend/dimension/chart.ts new file mode 100644 index 000000000..5017b5672 --- /dev/null +++ b/src/app/components/analysis/components/trend/dimension/chart.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { PropType, Ref } from "vue" +import type { DimensionEntry, ValueFormatter } from "@app/components/analysis/util" + +import { defineComponent, h, watch, onMounted, ref } from "vue" +import ChartWrapper from "./wrapper" + +const _default = defineComponent({ + props: { + data: Array as PropType, + title: String, + valueFormatter: Function as PropType + }, + setup(props) { + const elRef: Ref = ref() + const wrapper: ChartWrapper = new ChartWrapper() + const render = () => wrapper.render(props.data, props.title, props.valueFormatter) + + watch(() => props.data, render) + watch(() => props.valueFormatter, render) + + onMounted(() => { + wrapper.init(elRef.value) + render() + }) + + return () => h('div', { class: 'analysis-trend-dimension-chart', ref: elRef }) + } +}) + +export default _default diff --git a/src/app/components/analysis/components/trend/dimension/index.ts b/src/app/components/analysis/components/trend/dimension/index.ts new file mode 100644 index 000000000..984137ae6 --- /dev/null +++ b/src/app/components/analysis/components/trend/dimension/index.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { DimensionEntry, RingValue, ValueFormatter } from "@app/components/analysis/util" +import type { PropType } from "vue" + +import { computeRingText, formatValue } from "@app/components/analysis/util" +import { defineComponent, h } from "vue" +import Indicator from "../../common/indicator" +import Chart from "./chart" +import { cvt2LocaleTime } from "@app/util/time" + +type RenderProps = { + maxLabel: string + maxValue: number + averageLabel: string + average: RingValue + maxDate: string + valueFormatter: ValueFormatter +} + +const renderMax = ({ maxLabel, maxValue, valueFormatter: formatter, maxDate }: RenderProps) => + h('div', { class: 'analysis-trend-dimension-indicator-item' }, h(Indicator, { + mainName: maxLabel, + mainValue: formatter ? formatter(maxValue) : maxValue?.toString() || '-', + subValue: maxDate ? `@${cvt2LocaleTime(maxDate)}` : '', + })) + +const renderAverage = ({ averageLabel, valueFormatter, average }: RenderProps) => { + const currentAverage = average?.[0] + return h('div', { class: 'analysis-trend-dimension-indicator-item' }, h(Indicator, { + mainName: averageLabel, + mainValue: formatValue(currentAverage, valueFormatter), + subTips: msg => msg.analysis.common.ringGrowth, + subValue: computeRingText(average, valueFormatter), + })) +} + + +const _default = defineComponent({ + props: { + maxLabel: String, + maxValue: Number, + averageLabel: String, + averageValue: String, + maxDate: String, + average: [Object, Object] as PropType, + data: Array as PropType, + valueFormatter: Function as PropType, + dateRange: [Object, Object] as PropType<[Date, Date]>, + chartTitle: String, + }, + setup(props) { + return () => h('div', { class: "analysis-trend-dimension-container" }, [ + h('div', { class: 'analysis-trend-dimension-indicator-container' }, [ + renderMax(props), + renderAverage(props), + ]), + h('div', { class: 'analysis-trend-dimension-chart-container' }, + h(Chart, { data: props.data, valueFormatter: props.valueFormatter, title: props.chartTitle }) + ), + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/trend/dimension/wrapper.ts b/src/app/components/analysis/components/trend/dimension/wrapper.ts new file mode 100644 index 000000000..ee8d75307 --- /dev/null +++ b/src/app/components/analysis/components/trend/dimension/wrapper.ts @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { DimensionEntry } from "../../../util" +import type { ECharts, ComposeOption } from "echarts/core" +import type { LineSeriesOption } from "echarts/charts" +import type { + TitleComponentOption, + TooltipComponentOption, + ToolboxComponentOption, + GridComponentOption, +} from "echarts/components" + +import { init, use } from "@echarts/core" +import LineChart from "@echarts/chart/line" +import SVGRenderer from "@echarts/svg-renderer" +import TitleComponent from "@echarts/component/title" +import TooltipComponent from "@echarts/component/tooltip" +import GridComponent from '@echarts/component/grid' + +import { ValueFormatter } from "@app/components/analysis/util" +import { getSecondaryTextColor } from "@util/style" + +use([ + LineChart, + TitleComponent, + TooltipComponent, + GridComponent, + SVGRenderer, +]) + +type EcOption = ComposeOption< + | LineSeriesOption + | TitleComponentOption + | ToolboxComponentOption + | TooltipComponentOption + | GridComponentOption +> + +class ChartWrapper { + instance: ECharts + + init(container: HTMLDivElement) { + this.instance = init(container) + } + + async render(entries: DimensionEntry[], title: string, valueFormatter: ValueFormatter) { + const xAxis = entries.map(r => r.date) + const yAxis = entries.map(r => r.value) + + const secondaryTextColor = getSecondaryTextColor() + const option: EcOption = { + backgroundColor: 'rgba(0,0,0,0)', + title: { + text: title, + textStyle: { + color: secondaryTextColor, + fontSize: '14px', + fontWeight: 'normal', + }, + left: 'center', + top: '9%', + }, + grid: { + top: '30%', + bottom: '10px', + left: '5%', + right: '5%', + }, + tooltip: { + trigger: 'axis', + formatter(params: any) { + const format = params instanceof Array ? params[0] : params + const { name, value } = format + const valStr = valueFormatter?.(value) || value?.toString() || "NaN" + return `${name}
${valStr}` + }, + }, + xAxis: { + type: 'category', + data: xAxis, + show: false, + }, + yAxis: { + type: 'value', + axisLabel: { show: false }, + axisTick: { show: false }, + splitLine: { show: false }, + }, + series: { + data: yAxis, + type: 'line', + symbol: 'none', + areaStyle: {}, + smooth: true, + } + } + this.instance?.setOption(option) + } +} + +export default ChartWrapper diff --git a/src/app/components/analysis/components/trend/filter.ts b/src/app/components/analysis/components/trend/filter.ts new file mode 100644 index 000000000..ceb940422 --- /dev/null +++ b/src/app/components/analysis/components/trend/filter.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElementDatePickerShortcut } from "@src/element-ui/date" +import type { PropType, Ref } from "vue" + +import { t } from "@app/locale" +import { AnalysisMessage } from "@i18n/message/app/analysis" +import { ElDatePicker } from "element-plus" +import { defineComponent, h, ref } from "vue" +import { daysAgo } from "@util/time" + +function datePickerShortcut(msgKey: keyof AnalysisMessage['trend'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { + return { + text: t(msg => msg.analysis.trend[msgKey]), + value: daysAgo(agoOfStart - 1 || 0, agoOfEnd || 0) + } +} + +const SHORTCUTS = [ + datePickerShortcut('lastWeek', 7), + datePickerShortcut('last15Days', 15), + datePickerShortcut('last30Days', 30), + datePickerShortcut("last90Days", 90) +] + +const _default = defineComponent({ + props: { + dateRange: [Object, Object] as PropType<[Date, Date]> + }, + emits: { + dateRangeChange: (_val: [Date, Date]) => true + }, + setup(props, ctx) { + const dateFormat = t(msg => msg.calendar.dateFormat, { + y: 'YYYY', + m: 'MM', + d: 'DD' + }) + const dateRange: Ref<[Date, Date]> = ref(props.dateRange) + return () => h('div', { class: 'analysis-trend-filter' }, [ + h(ElDatePicker, { + modelValue: dateRange.value, + disabledDate: (date: Date) => date.getTime() > new Date().getTime(), + format: dateFormat, + type: 'daterange', + shortcuts: SHORTCUTS, + rangeSeparator: '-', + clearable: false, + 'onUpdate:modelValue': (newVal: [Date, Date]) => ctx.emit("dateRangeChange", dateRange.value = newVal), + }) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/trend/index.ts b/src/app/components/analysis/components/trend/index.ts new file mode 100644 index 000000000..ba1b3e0a3 --- /dev/null +++ b/src/app/components/analysis/components/trend/index.ts @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import type { DimensionEntry, ValueFormatter } from "@app/components/analysis/util" +import type { PropType, Ref, ComputedRef } from "vue" + +import { defineComponent, h, ref, watch, computed } from "vue" +import RowCard from "../common/row-card" +import Filter from "./filter" +import Total from "./total" +import Dimension from "./dimension" +import { t } from "@app/locale" +import './style.sass' +import { MILL_PER_DAY, daysAgo, getAllDatesBetween, getDayLenth } from "@util/time" +import { ElRow } from "element-plus" +import { cvt2LocaleTime, periodFormatter } from "@app/util/time" +import { groupBy } from "@util/array" + +type DailyIndicator = { + value: number + date: string +} + +type GlobalIndicator = number + +type DimensionType = 'focus' | 'visit' + +type IndicatorSet = Record & { + activeDay: number +} + +type SourceParam = { + dateRange: [Date, Date] + rows?: timer.stat.Row[] +} + +type EffectParam = { + indicators: Ref + lastIndicators: Ref + focusData: Ref + visitData: Ref +} + +const VISIT_MAX = t(msg => msg.analysis.trend.maxVisit) +const VISIT_AVE = t(msg => msg.analysis.trend.averageVisit) +const VISIT_CHART_TITLE = t(msg => msg.analysis.trend.visitTitle) +const FOCUS_MAX = t(msg => msg.analysis.trend.maxFocus) +const FOCUS_AVE = t(msg => msg.analysis.trend.averageFocus) +const FOCUS_CHART_TITLE = t(msg => msg.analysis.trend.focusTitle) + +function computeIndicatorSet(rows: timer.stat.Row[], dateRange: [Date, Date]): [IndicatorSet, Record] { + const [start, end] = dateRange || [] + const allDates = start && end ? getAllDatesBetween(start, end) : [] + if (!rows) { + // No data + return [undefined, groupBy(allDates, date => date, _l => undefined)] + } + + const days = allDates.length + const periodRows = rows.filter(({ date }) => allDates.includes(date)) + const periodRowMap = groupBy(periodRows, r => r.date, a => a[0]) + let focusMax: DailyIndicator + let visitMax: DailyIndicator + let focusTotal: number, visitTotal: number, activeDay: number + focusMax = visitMax = { date: undefined, value: undefined } + activeDay = focusTotal = visitTotal = 0 + + const fullPeriodRow: Record = {} + allDates.forEach(date => { + const row = periodRowMap[date] + if (!(fullPeriodRow[date] = row)) return + const { focus, time: visit } = row + focus > (focusMax.value ?? Number.MIN_SAFE_INTEGER) && (focusMax = { value: focus, date }) + visit > (visitMax.value ?? Number.MIN_SAFE_INTEGER) && (visitMax = { value: visit, date }) + focusTotal += focus + visitTotal += visit + focus && (activeDay += 1) + }) + + const indicators: IndicatorSet = { + activeDay, + focus: { max: focusMax, total: focusTotal, average: days == 0 ? undefined : focusTotal / days }, + visit: { max: visitMax, total: visitTotal, average: days == 0 ? undefined : visitTotal / days }, + } + return [indicators, fullPeriodRow] +} + +function lastRange(dateRange: [Date, Date]): [Date, Date] { + const [start, end] = dateRange || [] + if (!start || !end) return undefined + const dayLength = getDayLenth(start, end) + const newEnd = new Date(start.getTime() - MILL_PER_DAY) + const newStart = new Date(start.getTime() - MILL_PER_DAY * dayLength) + return [newStart, newEnd] +} + +const visitFormatter: ValueFormatter = (val: number) => { + if (Number.isInteger(val)) { + return val.toString() + } else { + return val?.toFixed(1) || '-' + } +} + +function handleDataChange(source: SourceParam, effect: EffectParam) { + const { dateRange, rows } = source + const { indicators, lastIndicators, focusData, visitData } = effect + // 1. this period + const [newIndicators, periodRows] = computeIndicatorSet(rows, dateRange) + indicators.value = newIndicators + const newFocusData: DimensionEntry[] = [] + const newVisitData: DimensionEntry[] = [] + Object.entries(periodRows) + .forEach(([rowDate, row]) => { + const { time, focus } = row || {} + const date = cvt2LocaleTime(rowDate) + newFocusData.push({ date, value: focus || 0 }) + newVisitData.push({ date, value: time || 0 }) + }) + focusData.value = newFocusData + visitData.value = newVisitData + // 2. last period + lastIndicators.value = computeIndicatorSet(rows, lastRange(dateRange))[0] +} + +const renderTotal = ( + indicators: IndicatorSet, + lastIndicators: IndicatorSet, + timeFormat: timer.app.TimeFormat, + rangeLength: number, +) => h('div', + { class: 'analysis-trend-content-col0' }, + h(Total, { + activeDay: [indicators?.activeDay, lastIndicators?.activeDay], + rangeLength: rangeLength, + visit: [indicators?.visit?.total, lastIndicators?.visit?.total], + focus: [indicators?.focus?.total, lastIndicators?.focus?.total], + timeFormat, + }) +) + +const renderFocusTrend = ( + indicators: IndicatorSet, + lastIndicators: IndicatorSet, + timeFormat: timer.app.TimeFormat, + data: DimensionEntry[], +) => h('div', + { class: 'analysis-trend-content-col1' }, + h(Dimension, { + maxLabel: FOCUS_MAX, + maxValue: indicators?.focus?.max?.value, + maxDate: indicators?.focus?.max?.date, + averageLabel: FOCUS_AVE, + average: [indicators?.focus?.average, lastIndicators?.focus?.average], + valueFormatter: (val: number) => val === undefined ? '-' : periodFormatter(val, timeFormat), + data, + chartTitle: FOCUS_CHART_TITLE, + }) +) + +const renderVisitTrend = ( + indicators: IndicatorSet, + lastIndicators: IndicatorSet, + data: DimensionEntry[], +) => h('div', + { class: 'analysis-trend-content-col2' }, + h(Dimension, { + maxLabel: VISIT_MAX, + maxValue: indicators?.visit?.max?.value, + maxDate: indicators?.visit?.max?.date, + averageLabel: VISIT_AVE, + average: [indicators?.visit?.average, lastIndicators?.visit?.average], + valueFormatter: visitFormatter, + data, + chartTitle: VISIT_CHART_TITLE, + }) +) + +const _default = defineComponent({ + props: { + rows: Array as PropType, + timeFormat: String as PropType, + }, + setup(props) { + const dateRange: Ref<[Date, Date]> = ref(daysAgo(29, 0)) + const visitData: Ref = ref([]) + const focusData: Ref = ref([]) + const indicators: Ref = ref() + const lastIndicators: Ref = ref() + const timeFormat: Ref = ref(props.timeFormat) + const rangeLength: ComputedRef = computed(() => getDayLenth(dateRange.value?.[0], dateRange.value?.[1])) + + const compute = () => handleDataChange( + { dateRange: dateRange.value, rows: props.rows }, + { indicators, lastIndicators, visitData, focusData } + ) + + watch(() => props.rows, compute) + watch(dateRange, compute) + watch(() => props.timeFormat, () => timeFormat.value = props.timeFormat) + + compute() + return () => h(RowCard, { + title: t(msg => msg.analysis.trend.title), + class: 'analysis-trend-container', + }, () => [ + h(Filter, { + dateRange: dateRange.value, + onDateRangeChange: (newVal: [Date, Date]) => dateRange.value = newVal, + }), + h(ElRow, { class: 'analysis-trend-content' }, () => [ + renderTotal(indicators.value, lastIndicators.value, timeFormat.value, rangeLength.value), + renderFocusTrend(indicators.value, lastIndicators.value, timeFormat.value, focusData.value), + renderVisitTrend(indicators.value, lastIndicators.value, visitData.value), + ]) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/components/trend/style.sass b/src/app/components/analysis/components/trend/style.sass new file mode 100644 index 000000000..18b95eef7 --- /dev/null +++ b/src/app/components/analysis/components/trend/style.sass @@ -0,0 +1,43 @@ +$divider: 1px var(--el-border-color) var(--el-border-style) + +.analysis-trend-container + .analysis-trend-filter + padding-top: 10px + padding-bottom: 14px + border-bottom: $divider + .analysis-trend-content + height: 380px + display: flex + .analysis-trend-content-col0 + flex: 1 1 0 + .analysis-trend-content-col1,.analysis-trend-content-col2 + flex: 2 1 0 + +.analysis-trend-total-container + display: flex + flex-direction: column + height: 100% + >div + width: 100% + flex: 1 + >div:not(:first-child) + border-top: $divider +.analysis-trend-dimension-container + display: flex + height: 100% + flex-direction: column + border-left: $divider + .analysis-trend-dimension-indicator-container + display: flex + flex: 1 1 0 + border-bottom: $divider + .analysis-trend-dimension-indicator-item + flex: 1 0 0 + >.analysis-trend-dimension-indicator-item:first-child + border-right: $divider + .analysis-trend-dimension-chart-container + display: flex + flex: 2 1 0 + .analysis-trend-dimension-chart + width: 100% + height: 100% diff --git a/src/app/components/analysis/components/trend/total.ts b/src/app/components/analysis/components/trend/total.ts new file mode 100644 index 000000000..1bf2ee4e0 --- /dev/null +++ b/src/app/components/analysis/components/trend/total.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { RingValue } from "@app/components/analysis/util" +import type { PropType } from "vue" +import type { I18nKey } from "@app/locale" +import type { IndicatorProps } from "../common/indicator" + +import { defineComponent, h } from "vue" +import Indicator from "../common/indicator" +import { t } from "@app/locale" +import { periodFormatter } from "@app/util/time" +import { computeRingText } from "@app/components/analysis/util" + +type Props = { + activeDay: RingValue + rangeLength: number + visit: RingValue + focus: RingValue + timeFormat: timer.app.TimeFormat +} + +const DAY_LABEL = `${t(msg => msg.analysis.trend.activeDay)}/${t(msg => msg.analysis.trend.totalDay)}` +const VISIT_LABEL = t(msg => msg.analysis.common.visitTotal) +const FOCUS_LABEL = t(msg => msg.analysis.common.focusTotal) +const RING_TIP: I18nKey = msg => msg.analysis.common.ringGrowth + +const computeDayValue = (props: Props) => { + const { activeDay, rangeLength } = props + const thisActiveDay = activeDay?.[0] + return `${thisActiveDay?.toString() || '-'}/${rangeLength?.toString() || '-'}` +} + +const renderIndicator = (props: IndicatorProps) => h('div', h(Indicator, props)) + +const computeFocusText = (focusRing: RingValue, format: timer.app.TimeFormat) => { + const current = focusRing?.[0] + return current === undefined ? '-' : periodFormatter(current, format) +} + +const _default = defineComponent({ + props: { + activeDay: [Object, Object] as PropType, + rangeLength: Number, + visit: [Object, Object] as PropType, + focus: [Object, Object] as PropType, + timeFormat: String as PropType + }, + setup(props) { + return () => h('div', { class: 'analysis-trend-total-container' }, [ + renderIndicator({ + mainName: DAY_LABEL, + mainValue: computeDayValue(props), + subTips: RING_TIP, + subValue: computeRingText(props.activeDay) + }), + renderIndicator({ + mainName: FOCUS_LABEL, + mainValue: computeFocusText(props.focus, props.timeFormat), + subTips: RING_TIP, + subValue: computeRingText(props.focus, delta => periodFormatter(delta, props.timeFormat)) + }), + renderIndicator({ + mainName: VISIT_LABEL, + mainValue: props.visit?.[0]?.toString() || '-', + subTips: RING_TIP, + subValue: computeRingText(props.visit), + }), + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/analysis/index.ts b/src/app/components/analysis/index.ts new file mode 100644 index 000000000..71241f3f0 --- /dev/null +++ b/src/app/components/analysis/index.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { Ref } from "vue" + +import { defineComponent, h, onMounted, watch, ref } from "vue" +import { useRoute, useRouter } from "vue-router" +import ContentContainer from "../common/content-container" +import Trend from "./components/trend" +import Filter from "./components/filter" +import Summary from "./components/summary" +import statService, { StatQueryParam } from "@service/stat-service" +import './style.sass' +import { judgeVirtualFast } from "@util/pattern" + +type _Queries = { + host: string + merge?: '1' | '0' +} + +function getSiteFromQuery(): timer.site.SiteInfo { + // Process the query param + const query: _Queries = useRoute().query as unknown as _Queries + useRouter().replace({ query: {} }) + const { host, merge } = query + // Init with queries + if (!host) { + return undefined + } + return { host, merged: merge === "1", virtual: judgeVirtualFast(host) } +} + +async function query(site: timer.site.SiteKey): Promise { + if (!site?.host) { + return [] + } + const param: StatQueryParam = { + host: site.host, + mergeHost: site?.merged || false, + fullHost: true, + sort: 'date', + sortOrder: 'ASC' + } + return await statService.select(param) +} + +const _default = defineComponent(() => { + const siteFromQuery = getSiteFromQuery() + const site: Ref = ref(siteFromQuery) + const timeFormat: Ref = ref('default') + const rows: Ref = ref() + const filter: Ref = ref() + + const queryInner = async () => { + const siteKey = site.value + rows.value = siteKey ? (await query(siteKey)) || [] : undefined + } + + onMounted(() => queryInner()) + watch(site, queryInner) + + return () => h(ContentContainer, {}, { + filter: () => h(Filter, { + site: site.value, + timeFormat: timeFormat.value, + ref: filter, + onSiteChange: (newSite: timer.site.SiteKey) => site.value = newSite, + onTimeFormatChange: (newFormat: timer.app.TimeFormat) => timeFormat.value = newFormat + }), + default: () => [ + h(Summary, { site: site.value, rows: rows.value, timeFormat: timeFormat.value }), + h(Trend, { rows: rows.value, timeFormat: timeFormat.value }), + ] + }) +}) + +export default _default diff --git a/src/app/components/analysis/style.sass b/src/app/components/analysis/style.sass new file mode 100644 index 000000000..f8b15cea2 --- /dev/null +++ b/src/app/components/analysis/style.sass @@ -0,0 +1,39 @@ +.analysis-row-card + margin-bottom: 15px + >.el-card__body + position: relative + .analysis-row-title + position: absolute + top: 5px + left: 10px + font-size: 12px + color: var(--el-text-color-regular) + z-index: 1000 + .analysis-row-body + padding-left: 5px + +.analysis-indicator-container + position: relative + top: 50% + transform: translateY(-50%) + padding-left: 40px + padding-top: 10px + padding-bottom: 10px + .indicator-name + font-size: 14px + color: var(--el-text-color-secondary) + .indicator-value + font-size: 24px + margin-block-end: 0.6em + margin-block-start: 0.25em + .indicator-sub-tip + height: 17px + line-height: 17px + font-size: 12px + word-break: break-word + color: var(--el-text-color-secondary) + .indicator-sub-value,.indicator-ring-growth-value + color: var(--el-text-color-primary) +.filter-container + .select-trigger + width: 240px diff --git a/src/app/components/analysis/util.ts b/src/app/components/analysis/util.ts new file mode 100644 index 000000000..c640b88fe --- /dev/null +++ b/src/app/components/analysis/util.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" + +/** + * Transfer host info to label + */ +export function labelOfHostInfo(site: timer.site.SiteKey): string { + if (!site) return '' + const { host, merged, virtual } = site + if (!host) return '' + let label = '' + merged && (label = `[${t(msg => msg.analysis.common.merged)}]`) + virtual && (label = `[${t(msg => msg.analysis.common.virtual)}]`) + return `${host}${label}` +} + +export type RingValue = [number, number] + +/** + * Compute ring text + * + * @param ring ring value + * @param fromatter formatter + * @returns text or '-' + */ +export function computeRingText(ring: RingValue, fromatter?: ValueFormatter): string { + if (!ring) { + return '-' + } + const [current, last] = ring + if (current === undefined && last === undefined) { + // return undefined if both are undefined + return '-' + } + const delta = (current || 0) - (last || 0) + let result = fromatter ? fromatter(delta) : delta?.toString() + delta >= 0 && (result = '+' + result) + return result +} + +export type ValueFormatter = (val: number) => string + +export const formatValue = (val: number, formatter?: ValueFormatter) => formatter ? formatter(val) : val?.toString() || '-' + +export type DimensionEntry = { + date: string + value: number +} \ No newline at end of file diff --git a/src/app/components/common/content-card.ts b/src/app/components/common/content-card.ts new file mode 100644 index 000000000..dfd1dddb8 --- /dev/null +++ b/src/app/components/common/content-card.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElCard } from "element-plus" +import { defineComponent, h, useSlots } from "vue" + +const _default = defineComponent(() => { + const { default: default_ } = useSlots() + return () => h(ElCard, { class: 'container-card' }, () => h(default_)) +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/common/content-container.ts b/src/app/components/common/content-container.ts index 0d4565e9e..1a255a98a 100644 --- a/src/app/components/common/content-container.ts +++ b/src/app/components/common/content-container.ts @@ -6,31 +6,20 @@ */ import { ElCard } from "element-plus" +import ContentCard from "./content-card" import { defineComponent, h, useSlots } from "vue" -const _default = defineComponent({ - name: "ContentContainer", - setup() { - const slots = useSlots() - const children = [] - const hasDefault = !!slots.default - if (hasDefault) { - // Only one content - children.push(h(slots.default)) - } else { - // Else filter and content - const hasFilter = !!slots.filter - if (hasFilter) { - children.push(h(ElCard, { class: "filter-container" }, () => h(slots.filter))) - } - slots.content && children.push(h(ElCard, { class: 'container-card' }, () => h(slots.content))) - } - - return () => h("div", - { class: "content-container" }, - children - ) +const _default = defineComponent(() => { + const slots = useSlots() + const children = [] + const { default: default_, filter, content } = slots + filter && children.push(h(ElCard, { class: "filter-container" }, () => h(filter))) + if (default_) { + children.push(h(slots.default)) + } else { + content && children.push(h(ContentCard, () => h(content))) } + return () => h("div", { class: "content-container" }, children) }) export default _default \ No newline at end of file diff --git a/src/app/components/habit/component/filter.ts b/src/app/components/habit/component/filter.ts index c55df34a1..889ab5d5b 100644 --- a/src/app/components/habit/component/filter.ts +++ b/src/app/components/habit/component/filter.ts @@ -36,8 +36,6 @@ function datePickerShortcut(msg: keyof HabitMessage['dateRange'], agoOfStart: nu const SHORTCUTS: ElementDatePickerShortcut[] = shortcutProps.map(([label, dayAgo]) => datePickerShortcut(label, dayAgo)) const AVERAGE_LABEL = t(msg => msg.habit.average.label) -const DATE_RANGE_START_PLACEHOLDER = t(msg => msg.trend.startDate) -const DATE_RANGE_END_PLACEHOLDER = t(msg => msg.trend.endDate) // [value, label] type _SizeOption = [number, keyof HabitMessage['sizes']] @@ -95,8 +93,6 @@ const _default = defineComponent({ }), // Date range picker h(DateRangeFilterItem, { - startPlaceholder: DATE_RANGE_START_PLACEHOLDER, - endPlaceholder: DATE_RANGE_END_PLACEHOLDER, clearable: false, disabledDate: (date: Date) => date.getTime() > new Date().getTime(), defaultRange: dateRange.value, diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index 66c7c458d..20b3b8a96 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -138,7 +138,6 @@ const _default = defineComponent({ t(msg => msg.option.backup.meta[DEFAULT.backupType].label) ) ] - console.log(type.value) type.value !== 'none' && nodes.push( h(ElDivider), renderOptionItem({ diff --git a/src/app/components/report/file-export.ts b/src/app/components/report/file-export.ts index 89dd325aa..7a6a87181 100644 --- a/src/app/components/report/file-export.ts +++ b/src/app/components/report/file-export.ts @@ -7,7 +7,7 @@ import { t } from "@app/locale" import { formatTime } from "@util/time" -import { periodFormatter } from "./formatter" +import { periodFormatter } from "@app/util/time" import { exportCsv as exportCsv_, exportJson as exportJson_, diff --git a/src/app/components/report/table/columns/date.ts b/src/app/components/report/table/columns/date.ts index baee04571..b51e1cf2e 100644 --- a/src/app/components/report/table/columns/date.ts +++ b/src/app/components/report/table/columns/date.ts @@ -11,7 +11,7 @@ import { ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" -import { dateFormatter } from "../../formatter" +import { cvt2LocaleTime } from "@app/util/time" const columnLabel = t(msg => msg.item.date) @@ -25,7 +25,7 @@ const _default = defineComponent({ align: "center", sortable: "custom" }, { - default: ({ row }: { row: timer.stat.Row }) => h('span', dateFormatter(row.date)) + default: ({ row }: { row: timer.stat.Row }) => h('span', cvt2LocaleTime(row.date)) }) } }) diff --git a/src/app/components/report/table/columns/focus.ts b/src/app/components/report/table/columns/focus.ts index 4389c637f..927d19e1f 100644 --- a/src/app/components/report/table/columns/focus.ts +++ b/src/app/components/report/table/columns/focus.ts @@ -10,7 +10,7 @@ import type { PropType } from "vue" import { t } from "@app/locale" import { Effect, ElTableColumn, ElTooltip } from "element-plus" import { defineComponent, h } from "vue" -import { periodFormatter } from "../../formatter" +import { periodFormatter } from "@app/util/time" import CompositionTable from './composition-table' const columnLabel = t(msg => msg.item.focus) diff --git a/src/app/components/report/table/columns/operation-delete-button.ts b/src/app/components/report/table/columns/operation-delete-button.ts index 8ffd44e54..ad0bff940 100644 --- a/src/app/components/report/table/columns/operation-delete-button.ts +++ b/src/app/components/report/table/columns/operation-delete-button.ts @@ -4,14 +4,14 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { PropType } from "vue" +import type { PropType, Ref } from "vue" -import { computed, defineComponent, h, Ref } from "vue" +import { computed, defineComponent, h } from "vue" import OperationPopupConfirmButton from "@app/components/common/popup-confirm-button" import { Delete } from "@element-plus/icons-vue" import { t } from "@app/locale" -import { dateFormatter } from "../../formatter" import { formatTime } from "@util/time" +import { cvt2LocaleTime } from "@app/util/time" const deleteButtonText = t(msg => msg.item.operation.delete) @@ -22,7 +22,7 @@ const deleteButtonText = t(msg => msg.item.operation.delete) * @param date item date */ function computeSingleConfirmText(url: string, date: string): string { - const formatDate = dateFormatter(date) + const formatDate = cvt2LocaleTime(date) return t(msg => msg.item.operation.deleteConfirmMsg, { url, date: formatDate }) } diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index 42cf1a773..db456a8f7 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -16,7 +16,7 @@ import StatDatabase from "@db/stat-database" import whitelistService from "@service/whitelist-service" import { t } from "@app/locale" import { LocationQueryRaw, Router, useRouter } from "vue-router" -import { TREND_ROUTE } from "@app/router/constants" +import { ANALYSIS_ROUTE } from "@app/router/constants" import { Open, Plus, Stopwatch } from "@element-plus/icons-vue" import OperationPopupConfirmButton from "@app/components/common/popup-confirm-button" import OperationDeleteButton from "./operation-delete-button" @@ -35,14 +35,20 @@ async function handleDeleteByRange(itemHost2Delete: string, dateRange: Array msg.item.operation.label) -const trendButtonText = t(msg => msg.item.operation.jumpToTrend) - +const COL_LABEL = t(msg => msg.item.operation.label) +const ANALYSIS = t(msg => msg.item.operation.analysis) // Whitelist texts -const add2WhitelistButtonText = t(msg => msg.item.operation.add2Whitelist) -const add2WhitelistSuccessMsg = t(msg => msg.report.added2Whitelist) -const removeFromWhitelistButtonText = t(msg => msg.item.operation.removeFromWhitelist) -const removeFromWhitelistSuccessMsg = t(msg => msg.report.removeFromWhitelist) +const ADD_WHITE = t(msg => msg.item.operation.add2Whitelist) +const ADD_WHITE_SUCC = t(msg => msg.report.added2Whitelist) +const REMOVE_WHITE = t(msg => msg.item.operation.removeFromWhitelist) +const REMOVE_WHITE_SUCC = t(msg => msg.report.removeFromWhitelist) + +const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { + en: 330, + zh_CN: 290, + ja: 360, + zh_TW: 290, +} const _default = defineComponent({ name: "OperationColumn", props: { @@ -57,15 +63,15 @@ const _default = defineComponent({ }, setup(props, ctx) { const canOperate = computed(() => !props.mergeHost) - const width = computed(() => props.mergeHost ? 110 : locale === "zh_CN" ? 290 : 330) + const width = computed(() => props.mergeHost ? 110 : LOCALE_WIDTH[locale]) const router: Router = useRouter() return () => h(ElTableColumn, { width: width.value, - label: columnLabel, + label: COL_LABEL, align: "center" }, { default: ({ row }: { row: timer.stat.Row }) => [ - // Trend + // Analysis h(ElButton, { icon: Stopwatch, size: 'small', @@ -75,9 +81,9 @@ const _default = defineComponent({ host: row.host, merge: props.mergeHost ? '1' : '0', } - router.push({ path: TREND_ROUTE, query }) + router.push({ path: ANALYSIS_ROUTE, query }) } - }, () => trendButtonText), + }, () => ANALYSIS), // Delete button h(OperationDeleteButton, { mergeDate: props.mergeDate, @@ -97,12 +103,12 @@ const _default = defineComponent({ h(OperationPopupConfirmButton, { buttonIcon: Plus, buttonType: "warning", - buttonText: add2WhitelistButtonText, + buttonText: ADD_WHITE, confirmText: t(msg => msg.whitelist.addConfirmMsg, { url: row.host }), visible: canOperate.value && !props.whitelist?.includes(row.host), async onConfirm() { await whitelistService.add(row.host) - ElMessage({ message: add2WhitelistSuccessMsg, type: 'success' }) + ElMessage({ message: ADD_WHITE_SUCC, type: 'success' }) ctx.emit("whitelistChange", row.host, true) } }), @@ -110,12 +116,12 @@ const _default = defineComponent({ h(OperationPopupConfirmButton, { buttonIcon: Open, buttonType: "primary", - buttonText: removeFromWhitelistButtonText, + buttonText: REMOVE_WHITE, confirmText: t(msg => msg.whitelist.removeConfirmMsg, { url: row.host }), visible: canOperate.value && props.whitelist?.includes(row.host), async onConfirm() { await whitelistService.remove(row.host) - ElMessage({ message: removeFromWhitelistSuccessMsg, type: 'success' }) + ElMessage({ message: REMOVE_WHITE_SUCC, type: 'success' }) ctx.emit("whitelistChange", row.host, false) } }) diff --git a/src/app/components/trend/components/chart/index.ts b/src/app/components/trend/components/chart/index.ts deleted file mode 100644 index 1ff38457b..000000000 --- a/src/app/components/trend/components/chart/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { Ref } from "vue" - -import { defineComponent, h, onMounted, ref } from "vue" -import ChartWrapper from "./wrapper" - -const _default = defineComponent({ - name: "TrendChart", - setup(_, ctx) { - const elRef: Ref = ref() - const chartWrapper: ChartWrapper = new ChartWrapper() - - function render(filterOption: TrendFilterOption, isOnMounted: boolean, row: timer.stat.Row[]) { - chartWrapper.render({ ...filterOption, isFirst: isOnMounted }, row) - } - - ctx.expose({ - render, - }) - - onMounted(() => chartWrapper.init(elRef.value)) - - return () => h('div', { class: 'chart-container', ref: elRef }) - } -}) - -export default _default diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts deleted file mode 100644 index 75ec9fa73..000000000 --- a/src/app/components/trend/components/chart/wrapper.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { ECharts, ComposeOption } from "echarts/core" -import type { LineSeriesOption } from "echarts/charts" -import type { - GraphicComponentOption, - GridComponentOption, - LegendComponentOption, - TitleComponentOption, - TooltipComponentOption, - ToolboxComponentOption, -} from "echarts/components" - -import { init, use } from "@echarts/core" -import LineChart from "@echarts/chart/line" -import GridComponent from "@echarts/component/grid" -import SVGRenderer from "@echarts/svg-renderer" -import LegendComponent from "@echarts/component/legend" -import TitleComponent from "@echarts/component/title" -import ToolboxComponent from "@echarts/component/toolbox" -import TooltipComponent from "@echarts/component/tooltip" - -import { t } from "@app/locale" -import { formatPeriodCommon, formatTime, MILL_PER_DAY } from "@util/time" -import siteService from "@service/site-service" -import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" -import { labelOfHostInfo } from "../common" - -use([ - LineChart, - GridComponent, - LegendComponent, - TitleComponent, - ToolboxComponent, - TooltipComponent, - SVGRenderer, -]) - -type EcOption = ComposeOption< - | LineSeriesOption - | GraphicComponentOption - | GridComponentOption - | LegendComponentOption - | TitleComponentOption - | ToolboxComponentOption - | TooltipComponentOption -> - -const TITLE = t(msg => msg.trend.history.title) -const DEFAULT_SUB_TITLE = t(msg => msg.trend.defaultSubTitle) -const SAVE_AS_IMAGE = t(msg => msg.trend.saveAsImageTitle) - -const NUMBER_UNIT = t(msg => msg.trend.history.numberUnit) - -const MILL_CONVERTERS: { [timeFormat in timer.app.TimeFormat]: (mill: number) => number } = { - default: mill => Math.floor(mill / 1000), - second: mill => Math.floor(mill / 1000), - minute: mill => mill / 1000 / 60, - hour: mill => mill / 1000 / 3600 -} - -function formatTimeOfEchart(params: any, timeFormat: timer.app.TimeFormat): string { - const format = params instanceof Array ? params[0] : params - const { seriesName, name, value } = format - let timeStr = '' - if (timeFormat === 'second') { - timeStr = (typeof value === 'number' ? value : 0).toFixed(0) + ' s' - } else if (timeFormat === 'minute') { - timeStr = (typeof value === 'number' ? value : 0).toFixed(1) + ' m' - } else if (timeFormat === 'hour') { - timeStr = (typeof value === 'number' ? value : 0).toFixed(2) + ' h' - } else { - const mills = (typeof value === 'number' ? value : 0) * 1000 - timeStr = formatPeriodCommon(mills) - } - return `${seriesName}
${name} - ${timeStr}` -} - -function optionOf( - xAxisData: string[], - subtext: string, - timeFormat: timer.app.TimeFormat, - [focusData, timeData]: [number[], number[]] -) { - const textColor = getPrimaryTextColor() - const secondaryTextColor = getSecondaryTextColor() - const option: EcOption = { - backgroundColor: 'rgba(0,0,0,0)', - grid: { top: '100' }, - title: { - text: TITLE, - textStyle: { color: textColor }, - subtext, - subtextStyle: { color: secondaryTextColor }, - left: 'center', - }, - tooltip: { trigger: 'item' }, - toolbox: { - feature: { - saveAsImage: { - show: true, - title: SAVE_AS_IMAGE, - excludeComponents: ['toolbox'], - pixelRatio: 1, - backgroundColor: '#fff', - iconStyle: { - borderColor: secondaryTextColor - } - } - } - }, - xAxis: { - type: 'category', - data: xAxisData, - axisLabel: { color: textColor }, - }, - yAxis: [ - { - name: t(msg => msg.trend.history.timeUnit[timeFormat || 'default']), - nameTextStyle: { color: textColor }, - type: 'value', - axisLabel: { color: textColor }, - }, - { - name: NUMBER_UNIT, - nameTextStyle: { color: textColor }, - type: 'value', - axisLabel: { color: textColor }, - } - ], - legend: [{ - left: 'left', - data: [t(msg => msg.item.focus), t(msg => msg.item.time)], - textStyle: { color: textColor }, - }], - series: [{ - name: t(msg => msg.item.focus), - data: focusData, - yAxisIndex: 0, - type: 'line', - smooth: true, - tooltip: { formatter: (params: any) => formatTimeOfEchart(params, timeFormat) } - }, { - name: t(msg => msg.item.time), - data: timeData, - yAxisIndex: 1, - type: 'line', - smooth: true, - tooltip: { - formatter: (params: any) => { - const format = params instanceof Array ? params[0] : params - const { seriesName, name, value } = format - return `${seriesName}
${name} - ${value}` - } - } - }] - } - return option -} - - -// Get the timestamp of one timestamp of date -const timestampOf = (d: Date) => d.getTime() - -/** -* Get the x-axis of date -*/ -function getAxias(format: string, dateRange: Date[] | undefined): string[] { - if (!dateRange || !dateRange.length) { - // @since 0.0.9 - // The dateRange is cleared, return empty data - return [] - } - const xAxisData = [] - const startTime = timestampOf(dateRange[0]) - const endTime = timestampOf(dateRange[1]) - for (let time = startTime; time <= endTime; time += MILL_PER_DAY) { - xAxisData.push(formatTime(time, format)) - } - return xAxisData -} - -async function processSubtitle(host: TrendHostInfo) { - let subtitle = labelOfHostInfo(host) - if (!subtitle) { - return DEFAULT_SUB_TITLE - } - if (!host.merged) { - // If not merged, append the site name to the original subtitle - // @since 0.9.0 - const siteInfo: timer.site.SiteInfo = await siteService.get(host) - const siteName = siteInfo?.alias - siteName && (subtitle += ` / ${siteName}`) - } - return subtitle -} - -function processDataItems(allDates: string[], timeFormat: timer.app.TimeFormat, rows: timer.stat.Row[]): [number[], number[]] { - timeFormat = timeFormat || 'default' - const millConverter = MILL_CONVERTERS[timeFormat] - const focusData: number[] = [] - const timeData: number[] = [] - - const dateInfoMap: Record = {} - rows.forEach(row => dateInfoMap[row.date] = row) - - allDates.forEach(date => { - const row = dateInfoMap[date] - focusData.push(millConverter(row?.focus || 0)) - timeData.push(row?.time || 0) - }) - return [focusData, timeData] -} - -class ChartWrapper { - instance: ECharts - - init(container: HTMLDivElement) { - this.instance = init(container) - } - - async render(renderOption: TrendRenderOption, rows: timer.stat.Row[]) { - const { host, dateRange, timeFormat } = renderOption - // 1. x-axis data - let xAxisData: string[], allDates: string[] - if (!dateRange || dateRange.length !== 2) { - xAxisData = [] - allDates = [] - } else { - xAxisData = getAxias('{m}/{d}', dateRange) - allDates = getAxias('{y}{m}{d}', dateRange) - } - - // 2. subtitle - const subtitle = await processSubtitle(host) - - // 3. series data - const dataItems = processDataItems(allDates, timeFormat, rows) - - const option: EcOption = optionOf(xAxisData, subtitle, timeFormat, dataItems) - - this.instance?.setOption(option) - } -} - -export default ChartWrapper diff --git a/src/app/components/trend/components/common.ts b/src/app/components/trend/components/common.ts deleted file mode 100644 index 688230561..000000000 --- a/src/app/components/trend/components/common.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { t } from "@app/locale" - -/** - * Transfer host info to label - */ -export function labelOfHostInfo(hostInfo: TrendHostInfo): string { - if (!hostInfo) return '' - const { host, merged, virtual } = hostInfo - if (!host) return '' - 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 deleted file mode 100644 index f9ac3de09..000000000 --- a/src/app/components/trend/components/filter.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { Ref, PropType, VNode } from "vue" - -import { ElOption, ElSelect, ElTag } from "element-plus" -import { ref, h, defineComponent } from "vue" -import statService, { HostSet } from "@service/stat-service" -import { daysAgo } from "@util/time" -import { t } from "@app/locale" -import { TrendMessage } from "@i18n/message/app/trend" -import DateRangeFilterItem from "@app/components/common/date-range-filter-item" -import SelectFilterItem from "@app/components/common/select-filter-item" -import { ElementDatePickerShortcut } from "@src/element-ui/date" -import { labelOfHostInfo } from "./common" - -async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { - if (!queryStr) { - trendDomainOptions.value = [] - return - } - searching.value = true - const domains: HostSet = await statService.listHosts(queryStr) - const options: TrendHostInfo[] = [] - 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 -} - -function datePickerShortcut(msg: keyof TrendMessage, agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { - return { - text: t(messages => messages.trend[msg]), - value: daysAgo(agoOfStart || 0, agoOfEnd || 0) - } -} - -const SHORTCUTS = [ - datePickerShortcut('lastWeek', 7), - datePickerShortcut('last15Days', 15), - datePickerShortcut('last30Days', 30), - datePickerShortcut("last90Days", 90) -] - -const HOST_PLACEHOLDER = t(msg => msg.trend.hostPlaceholder) -// Date picker -const START_DATE_PLACEHOLDER = t(msg => msg.trend.startDate) -const END_DATE_PLACEHOLDER = t(msg => msg.trend.endDate) - -const TIME_FORMAT_LABELS: { [key in timer.app.TimeFormat]: string } = { - default: t(msg => msg.timeFormat.default), - second: t(msg => msg.timeFormat.second), - minute: t(msg => msg.timeFormat.minute), - hour: t(msg => msg.timeFormat.hour) -} - -function keyOfHostInfo(option: TrendHostInfo): string { - 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, 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) - ] - hostInfo.merged && result.push( - h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) - ) - hostInfo.virtual && result.push( - h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT) - ) - return result -} - -const _default = defineComponent({ - name: "TrendFilter", - props: { - dateRange: Object as PropType, - defaultValue: Object as PropType, - timeFormat: String as PropType - }, - emits: { - change: (_option: TrendFilterOption) => true - }, - setup(props, ctx) { - // @ts-ignore - const dateRange: Ref = ref(props.dateRange) - const domainKey: Ref = ref('') - const trendSearching: Ref = ref(false) - const trendDomainOptions: Ref = ref([]) - const defaultOption: TrendHostInfo = props.defaultValue - const timeFormat: Ref = ref(props.timeFormat) - if (defaultOption) { - domainKey.value = keyOfHostInfo(defaultOption) - trendDomainOptions.value.push(defaultOption) - } - - function handleChange() { - const hostInfo: TrendHostInfo = hostInfoOfKey(domainKey.value) - const option: TrendFilterOption = { - host: hostInfo, - dateRange: dateRange.value, - timeFormat: timeFormat.value - } - ctx.emit('change', option) - } - - return () => [h(ElSelect, { - placeholder: HOST_PLACEHOLDER, - class: 'filter-item', - modelValue: domainKey.value, - clearable: true, - filterable: true, - remote: true, - loading: trendSearching.value, - remoteMethod: (query: string) => handleRemoteSearch(query, trendDomainOptions, trendSearching), - onChange: (key: string) => { - domainKey.value = key - handleChange() - }, - onClear: () => { - domainKey.value = '' - handleChange() - } - }, () => (trendDomainOptions.value || [])?.map( - hostInfo => h(ElOption, { - value: keyOfHostInfo(hostInfo), - label: labelOfHostInfo(hostInfo), - }, () => renderHostLabel(hostInfo)) - )), - h(DateRangeFilterItem, { - defaultRange: dateRange.value, - startPlaceholder: START_DATE_PLACEHOLDER, - endPlaceholder: END_DATE_PLACEHOLDER, - shortcuts: SHORTCUTS, - onChange: (newVal: Date[]) => { - dateRange.value = newVal - handleChange() - }, - clearable: false, - disabledDate: (date: Date) => date.getTime() > new Date().getTime(), - }), - h(SelectFilterItem, { - historyName: 'timeFormat', - defaultValue: timeFormat.value, - options: TIME_FORMAT_LABELS, - onSelect(newVal: timer.app.TimeFormat) { - timeFormat.value = newVal - handleChange() - } - })] - } -}) - -export default _default \ No newline at end of file diff --git a/src/app/components/trend/index.ts b/src/app/components/trend/index.ts deleted file mode 100644 index df4c9637e..000000000 --- a/src/app/components/trend/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import type { Ref } from "vue" - -import { defineComponent, h, onMounted, ref } from "vue" -import { useRoute, useRouter } from "vue-router" -import { daysAgo } from "@util/time" -import ContentContainer from "../common/content-container" -import TrendChart from "./components/chart" -import TrendFilter from "./components/filter" -import statService, { StatQueryParam } from "@service/stat-service" - -type _Queries = { - host: string - merge: '1' | '0' | undefined -} - -function initWithQuery(hostOption: Ref) { - // Process the query param - const query: _Queries = useRoute().query as unknown as _Queries - useRouter().replace({ query: {} }) - const { host, merge } = query - // Init with queries - host && (hostOption.value = { host, merged: merge === "1" }) -} - -async function query(hostOption: Ref, dateRange: Ref): Promise { - const hostVal = hostOption.value?.host - if (!hostVal) { - return [] - } - const param: StatQueryParam = { - // If the host is empty, no result will be queried with this param. - host: hostVal, - mergeHost: hostOption.value?.merged || false, - date: dateRange.value, - fullHost: true, - sort: 'date', - sortOrder: 'ASC' - } - return await statService.select(param) -} - -const _default = defineComponent({ - name: "Trend", - setup() { - // @ts-ignore - const dateRange: Ref = ref(daysAgo(7, 0)) - const hostOption: Ref = ref() - const timeFormat: Ref = ref('default') - const chart: Ref = ref() - const filter: Ref = ref() - - initWithQuery(hostOption) - - async function queryAndRender(isOnMounted?: boolean) { - const row = await query(hostOption, dateRange) - const filterOption: TrendFilterOption = { - host: hostOption.value, - dateRange: dateRange.value, - timeFormat: timeFormat.value - } - chart.value?.render(filterOption, !!isOnMounted, row) - } - - onMounted(() => queryAndRender(true)) - - return () => h(ContentContainer, {}, { - filter: () => h(TrendFilter, { - defaultValue: hostOption.value, - timeFormat: timeFormat.value, - dateRange: dateRange.value, - ref: filter, - onChange(option: TrendFilterOption) { - hostOption.value = option.host - dateRange.value = option.dateRange - timeFormat.value = option.timeFormat - queryAndRender() - } - }), - content: () => h(TrendChart, { ref: chart }) - }) - } -}) - -export default _default diff --git a/src/app/components/trend/trend.d.ts b/src/app/components/trend/trend.d.ts deleted file mode 100644 index 69fc5c1e4..000000000 --- a/src/app/components/trend/trend.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare type TrendHostInfo = { - host: string - merged?: boolean - virtual?: boolean -} - -declare type TrendFilterOption = { - host: TrendHostInfo, - dateRange: Date[], - timeFormat: timer.app.TimeFormat -} - -declare type TrendRenderOption = TrendFilterOption & { - /** - * Whether render firstly - */ - isFirst: boolean -} \ No newline at end of file diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 1bb13eab6..624db8210 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -20,6 +20,7 @@ import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Memo, Rank, Se import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" import { createTab } from "@api/chrome/tab" +import { ANALYSIS_ROUTE } from "@app/router/constants" type _MenuItem = { title: keyof MenuMessage @@ -79,8 +80,8 @@ function generateMenus(): _MenuGroup[] { route: '/data/report', icon: Calendar }, { - title: 'dataHistory', - route: '/data/history', + title: 'siteAnalysis', + route: ANALYSIS_ROUTE, icon: TrendIcon }, { title: 'dataClear', diff --git a/src/app/router/constants.ts b/src/app/router/constants.ts index 07cf2127f..242c15216 100644 --- a/src/app/router/constants.ts +++ b/src/app/router/constants.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -export const TREND_ROUTE = '/data/history' +export const ANALYSIS_ROUTE = '/data/analysis' export const OPTION_ROUTE = '/additional/option' diff --git a/src/app/router/index.ts b/src/app/router/index.ts index dace0236a..f11dd70e1 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -9,7 +9,7 @@ import type { App } from "vue" import type { RouteRecordRaw } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router" -import { OPTION_ROUTE, TREND_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" +import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" import metaService from "@service/meta-service" const dataRoutes: RouteRecordRaw[] = [ @@ -26,8 +26,8 @@ const dataRoutes: RouteRecordRaw[] = [ path: REPORT_ROUTE, component: () => import('../components/report') }, { - path: TREND_ROUTE, - component: () => import('../components/trend') + path: ANALYSIS_ROUTE, + component: () => import('../components/analysis') }, { path: '/data/manage', component: () => import('../components/data-manage') diff --git a/src/app/styles/index.sass b/src/app/styles/index.sass index 5e66a628e..e5cce78d7 100644 --- a/src/app/styles/index.sass +++ b/src/app/styles/index.sass @@ -54,6 +54,7 @@ a overflow-y: auto .filter-container + margin-bottom: 15px display: flex align-items: center user-select: none @@ -110,10 +111,7 @@ a align-items: center margin-top: 23px -// charts - .container-card - margin-top: 15px min-height: 640px .el-card__body height: 100% diff --git a/src/app/components/report/formatter.ts b/src/app/util/time.ts similarity index 90% rename from src/app/components/report/formatter.ts rename to src/app/util/time.ts index c45cb48f2..2022b1319 100644 --- a/src/app/components/report/formatter.ts +++ b/src/app/util/time.ts @@ -9,10 +9,12 @@ import { t } from "@app/locale" import { formatPeriodCommon, MILL_PER_MINUTE } from "@util/time" /** - * @param date date string {yyyy}{mm}{dd} - * @returns the msg + * Convert {yyyy}{mm}{dd} to locale time + * + * @param date {yyyy}{mm}{dd} */ -export function dateFormatter(date: string): string { +export function cvt2LocaleTime(date: string) { + if (!date) return '-' const y = date.substring(0, 4) const m = date.substring(4, 6) const d = date.substring(6, 8) diff --git a/src/i18n/message/app/analysis.ts b/src/i18n/message/app/analysis.ts new file mode 100644 index 000000000..21b186a67 --- /dev/null +++ b/src/i18n/message/app/analysis.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type AnalysisMessage = { + common: { + focusTotal: string + visitTotal: string + ringGrowth: string + merged: string + virtual: string + hostPlaceholder: string + emptyDesc: string + } + summary: { + title: string + day: string + firstDay: string + } + trend: { + title: string + startDate: string, + endDate: string + lastWeek: string + last15Days: string + last30Days: string + last90Days: string + activeDay: string + totalDay: string + maxFocus: string + averageFocus: string + maxVisit: string + averageVisit: string + focusTitle: string + visitTitle: string + } +} + +const _default: Messages = { + zh_CN: { + common: { + focusTotal: '总计浏览时长', + visitTotal: '总计访问次数', + ringGrowth: '与上期相比 {value}', + merged: '合并', + virtual: '自定义', + hostPlaceholder: '搜索你想分析的站点', + emptyDesc: '未选择站点' + }, + summary: { + title: '数据总览', + day: '总计活跃天数', + firstDay: '首次访问 {value}', + }, + trend: { + title: '区间趋势', + startDate: '开始日期', + endDate: '结束日期', + lastWeek: '最近 7 天', + last15Days: '最近 15 天', + last30Days: '最近 30 天', + last90Days: '最近 90 天', + activeDay: '活跃天数', + totalDay: '区间总天数', + maxFocus: '单日最大浏览时长', + averageFocus: '单日平均浏览时长', + maxVisit: '单日最大访问次数', + averageVisit: '单日平均访问次数', + focusTitle: '浏览时长趋势', + visitTitle: '访问次数趋势', + } + }, + zh_TW: { + common: { + focusTotal: '總計瀏覽時長', + visitTotal: '總計訪問次數', + ringGrowth: '與前期相比 {value}', + merged: '合並', + virtual: '自定義', + hostPlaceholder: '蒐索你想分析的站點', + emptyDesc: '未選擇站點', + }, + summary: { + title: '數據總覽', + day: '總計活躍天數', + firstDay: '首次訪問 {value}', + }, + trend: { + title: '區間趨勢', + startDate: '開始日期', + endDate: '結束日期', + lastWeek: '最近 7 天', + last15Days: '最近 15 天', + last30Days: '最近 30 天', + last90Days: '最近 90 天', + activeDay: '活躍天數', + totalDay: '區間總天數', + maxFocus: '單日最大瀏覽時長', + averageFocus: '單日平均瀏覽時長', + maxVisit: '單日最大訪問次數', + averageVisit: '單日平均訪問次數', + focusTitle: '瀏覽時長趨勢', + visitTitle: '訪問次數趨勢', + } + }, + en: { + common: { + focusTotal: 'Total browsing time', + visitTotal: 'Total visits', + ringGrowth: '{value} compared to the previous period', + merged: 'Merged', + virtual: 'Virtual', + hostPlaceholder: 'Search for a site to analyze', + emptyDesc: 'No site selected', + }, + summary: { + title: 'Summary', + day: 'Total active days', + firstDay: 'First visit {value}', + }, + trend: { + title: 'Trends', + startDate: 'Start date', + endDate: 'End date', + lastWeek: 'Last week', + last15Days: 'Last 15 days', + last30Days: 'Last 30 days', + last90Days: 'Last 90 days', + activeDay: 'Active days', + totalDay: 'Period days', + maxFocus: 'Daily maximum browsing time', + averageFocus: 'Daily average browsing time', + maxVisit: 'Daily maximum visits', + averageVisit: 'Daily average visits', + focusTitle: 'Browsing Time Trends', + visitTitle: 'Visit Trends', + } + }, + ja: { + common: { + focusTotal: '総閲覧時間', + visitTotal: '総訪問数', + ringGrowth: '前期比 {value}', + merged: '合并', + virtual: 'カスタマイズ', + hostPlaceholder: 'ドメイン名を検索', + emptyDesc: 'サイトは空です', + }, + summary: { + title: 'Summary', + day: 'Total active days', + firstDay: 'First visit {value}', + }, + trend: { + title: 'レンジトレンド', + startDate: '開始日', + endDate: '終了日', + lastWeek: '過去 7 日間', + last15Days: '過去 15 日間', + last30Days: '過去 30 日間', + last90Days: '過去 90 日間', + activeDay: 'アクティブな日', + totalDay: '間隔の合計日数', + maxFocus: '1 日の最大閲覧時間', + averageFocus: '1 日あたりの平均閲覧時間', + maxVisit: '1 日あたりの最大訪問数', + averageVisit: '1 日あたりの平均訪問数', + focusTitle: 'タイム トレンドの閲覧', + visitTitle: '訪問数の傾向', + } + } +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 1a4f4ba46..5a7e79566 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -8,7 +8,7 @@ import itemMessages, { ItemMessage } from "@i18n/message/common/item" import dataManageMessages, { DataManageMessage } from "./data-manage" import reportMessages, { ReportMessage } from "./report" -import trendMessages, { TrendMessage } from "./trend" +import analysisMessages, { AnalysisMessage } from "./analysis" import menuMessages, { MenuMessage } from "./menu" import habitMessages, { HabitMessage } from "./habit" import limitMessages, { LimitMessage } from "./limit" @@ -31,7 +31,7 @@ export type AppMessage = { whitelist: WhitelistMessage mergeRule: MergeRuleMessage option: OptionMessage - trend: TrendMessage + analysis: AnalysisMessage menu: MenuMessage habit: HabitMessage limit: LimitMessage @@ -53,7 +53,7 @@ const _default: Messages = { whitelist: whitelistMessages.zh_CN, mergeRule: mergeRuleMessages.zh_CN, option: optionMessages.zh_CN, - trend: trendMessages.zh_CN, + analysis: analysisMessages.zh_CN, menu: menuMessages.zh_CN, habit: habitMessages.zh_CN, limit: limitMessages.zh_CN, @@ -73,7 +73,7 @@ const _default: Messages = { whitelist: whitelistMessages.zh_TW, mergeRule: mergeRuleMessages.zh_TW, option: optionMessages.zh_TW, - trend: trendMessages.zh_TW, + analysis: analysisMessages.zh_TW, menu: menuMessages.zh_TW, habit: habitMessages.zh_TW, limit: limitMessages.zh_TW, @@ -93,7 +93,7 @@ const _default: Messages = { whitelist: whitelistMessages.en, mergeRule: mergeRuleMessages.en, option: optionMessages.en, - trend: trendMessages.en, + analysis: analysisMessages.en, menu: menuMessages.en, habit: habitMessages.en, limit: limitMessages.en, @@ -113,7 +113,7 @@ const _default: Messages = { whitelist: whitelistMessages.ja, mergeRule: mergeRuleMessages.ja, option: optionMessages.ja, - trend: trendMessages.ja, + analysis: analysisMessages.ja, menu: menuMessages.ja, habit: habitMessages.ja, limit: limitMessages.ja, diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index e5b3847cf..aee5d5e74 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -9,7 +9,7 @@ export type MenuMessage = { dashboard: string data: string dataReport: string - dataHistory: string + siteAnalysis: string dataClear: string behavior: string habit: string @@ -31,7 +31,7 @@ const _default: Messages = { dashboard: '仪表盘', data: '我的数据', dataReport: '报表明细', - dataHistory: '历史趋势', + siteAnalysis: '站点分析', dataClear: '内存管理', additional: '附加功能', siteManage: '网站管理', @@ -51,7 +51,7 @@ const _default: Messages = { dashboard: '儀錶盤', data: '我的數據', dataReport: '報表明細', - dataHistory: '曆史趨勢', + siteAnalysis: '站點分析', dataClear: '內存管理', additional: '附加功能', siteManage: '網站管理', @@ -71,7 +71,7 @@ const _default: Messages = { dashboard: 'Dashboard', data: 'My Data', dataReport: 'Record', - dataHistory: 'Trend', + siteAnalysis: 'Site Analysis', dataClear: 'Memory Situation', behavior: 'User Behavior', habit: 'Habits', @@ -91,7 +91,7 @@ const _default: Messages = { dashboard: 'ダッシュボード', data: '私のデータ', dataReport: '報告する', - dataHistory: '歴史傾向', + siteAnalysis: 'ウェブサイト分析', dataClear: '記憶状況', behavior: 'ユーザーの行動', habit: '閲覧の習慣', diff --git a/src/i18n/message/app/trend.ts b/src/i18n/message/app/trend.ts deleted file mode 100644 index 5aa9b77d7..000000000 --- a/src/i18n/message/app/trend.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -export type TrendMessage = { - hostPlaceholder: string - startDate: string, - endDate: string - lastWeek: string - last15Days: string - last30Days: string - last90Days: string - history: { - title: string - timeUnit: { [key in timer.app.TimeFormat]: string } - numberUnit: string - } - saveAsImageTitle: string - defaultSubTitle: string - merged: string - virtual: string -} - -const _default: Messages = { - zh_CN: { - hostPlaceholder: '搜索你想分析的域名', - startDate: '开始日期', - endDate: '结束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - history: { - title: '历史记录', - timeUnit: { - default: '时长 / 秒', - second: '时长 / 秒', - minute: '时长 / 分钟', - hour: '时长 / 小时', - }, - numberUnit: '次', - }, - saveAsImageTitle: '保存', - defaultSubTitle: '请先在左上角选择需要分析的域名', - merged: '合并', - virtual: '自定义', - }, - zh_TW: { - hostPlaceholder: '蒐索你想分析的網域', - startDate: '開始日期', - endDate: '結束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - history: { - title: '曆史記錄', - timeUnit: { - default: '時長 / 秒', - second: '時長 / 秒', - minute: '時長 / 分鐘', - hour: '時長 / 小時', - }, - numberUnit: '次', - }, - saveAsImageTitle: '保存', - defaultSubTitle: '請先在左上角選擇需要分析的網域', - merged: '合並', - virtual: '自定義', - }, - en: { - hostPlaceholder: 'Search site URL', - startDate: 'Start date', - endDate: 'End date', - lastWeek: 'Last week', - last15Days: 'Last 15 days', - last30Days: 'Last 30 days', - last90Days: 'Last 90 days', - history: { - title: 'Trend', - timeUnit: { - default: 'Time / second', - second: 'Time / second', - minute: 'Time / minute', - hour: 'Time / hour', - }, - numberUnit: 'Visit Counts', - }, - saveAsImageTitle: 'Snapshot', - defaultSubTitle: 'Search and select one URL to analyze on the top-left corner, pls', - merged: 'Merged', - virtual: 'Virtual', - }, - ja: { - hostPlaceholder: 'ドメイン名を検索', - startDate: '開始日', - endDate: '終了日', - lastWeek: '先週', - last15Days: '過去 15 日間', - last30Days: '過去 30 日間', - last90Days: '過去 90 日間', - history: { - title: '歴史記録', - timeUnit: { - default: '期間 / 秒', - second: '期間 / 秒', - minute: '期間 / 分', - hour: '期間 / 時間', - }, - numberUnit: '回', - }, - saveAsImageTitle: 'ダウンロード', - defaultSubTitle: 'まず、左上隅で分析するドメイン名を選択します', - merged: '合并', - virtual: 'カスタマイズ', - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/common/item.ts b/src/i18n/message/common/item.ts index 0faab152f..c5355512c 100644 --- a/src/i18n/message/common/item.ts +++ b/src/i18n/message/common/item.ts @@ -18,7 +18,7 @@ export type ItemMessage = { deleteConfirmMsgAll: string deleteConfirmMsgRange: string deleteConfirmMsg: string - jumpToTrend: string + analysis: string exportWholeData: string importWholeData: string } @@ -35,7 +35,7 @@ const _default: Messages = { delete: '删除', add2Whitelist: '白名单', removeFromWhitelist: '启用', - jumpToTrend: '趋势', + analysis: '分析', deleteConfirmMsgAll: '{url} 的所有访问记录将被删除', deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的访问记录将被删除', deleteConfirmMsg: '{url} 在 {date} 的访问记录将被删除', @@ -53,7 +53,7 @@ const _default: Messages = { delete: '刪除', add2Whitelist: '白名單', removeFromWhitelist: '啟用', - jumpToTrend: '趨勢', + analysis: '分析', deleteConfirmMsgAll: '{url} 的所有拜訪記錄將被刪除', deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的拜訪記錄將被刪除', deleteConfirmMsg: '{url} 在 {date} 的拜訪記錄將被刪除', @@ -71,7 +71,7 @@ const _default: Messages = { delete: 'Delete', add2Whitelist: 'Whitelist', removeFromWhitelist: 'Enable', - jumpToTrend: 'Trend', + analysis: 'Analysis', deleteConfirmMsgAll: 'All records of {url} will be deleted!', deleteConfirmMsgRange: 'All records of {url} between {start} and {end} will be deleted!', deleteConfirmMsg: 'The record of {url} on {date} will be deleted!', @@ -89,7 +89,7 @@ const _default: Messages = { delete: '削除', add2Whitelist: 'ホワイトリスト', removeFromWhitelist: '有効にする', - jumpToTrend: '傾向', + analysis: '分析する', deleteConfirmMsgAll: '{url} のすべての拜訪記録が削除されます', deleteConfirmMsgRange: '{url} {start} から {end} までの拜訪記録は削除されます', deleteConfirmMsg: '{date} の {url} の拜訪記録は削除されます', diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index f5ccb3866..b80f904bf 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -104,8 +104,7 @@ class StatService { */ 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)) + const allHosts: Set = new Set(rows.map(row => row.host)) // Generate ruler const mergeRuleItems: timer.merge.Rule[] = await mergeRuleDatabase.selectAll() const mergeRuler = new CustomizedHostMergeRuler(mergeRuleItems) @@ -125,7 +124,11 @@ class StatService { }) const virtualSites = await siteDatabase.select({ virtual: true }) - const virtual: Set = new Set(virtualSites.map(site => site.host)) + const virtual: Set = new Set( + virtualSites + .map(site => site.host) + .filter(host => host?.includes(fuzzyQuery)) + ) return { origin, merged, virtual } } diff --git a/src/util/time.ts b/src/util/time.ts index 0a4757b0b..9954cbe47 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -213,4 +213,24 @@ export function getDayLenth(dateStart: Date, dateEnd: Date): number { } while (cursor.getTime() < dateEnd.getTime()) isSameDay(cursor, dateEnd) && dateDiff++ return dateDiff +} + +/** + * Calc the dates between {@param dateStart} and {@param dateEnd} + * + * @returns + * [20220609] if 2022-06-09 00:00:00 to 2022-06-09 00:00:01 + * [] if 2022-06-10 00:00:00 to 2022-06-09 00:00:01 + * [20221110, 20221111] if 2022-11-10 08:00:00 to 2022-11-11 00:00:01 + */ +export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { + const format = '{y}{m}{d}' + let cursor = new Date(dateStart) + let dates = [] + do { + dates.push(formatTime(cursor, format)) + cursor = new Date(cursor.getTime() + MILL_PER_DAY) + } while (cursor.getTime() < dateEnd.getTime()) + isSameDay(cursor, dateEnd) && dates.push(formatTime(dateEnd, format)) + return dates } \ No newline at end of file From b567ea217d7c004481a65c7aa2a37fba2820bdfb Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 9 Apr 2023 00:27:14 +0800 Subject: [PATCH 131/168] v1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc3ed82a2..f8dcac325 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.6.1", + "version": "1.7.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From 0689b70e41ca56be220653b5a4b828156f344b5b Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 10 Apr 2023 16:20:43 +0800 Subject: [PATCH 132/168] Fix ring time error --- src/util/time.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/util/time.ts b/src/util/time.ts index 9954cbe47..4e6edacef 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -59,6 +59,8 @@ export function formatTime(time: Date | string | number, cFormat?: string) { * Format milliseconds for display */ export function formatPeriod(milliseconds: number, message: { hourMsg: string, minuteMsg: string, secondMsg: string }): string { + const prefix = milliseconds < 0 ? '-' : '' + milliseconds = Math.abs(milliseconds) const { hourMsg, minuteMsg, secondMsg } = message const seconds = Math.floor(milliseconds / 1000) const hour = Math.floor(seconds / 3600) @@ -67,7 +69,10 @@ export function formatPeriod(milliseconds: number, message: { hourMsg: string, m let msg = hourMsg hour === 0 && (msg = minuteMsg) && minute === 0 && (msg = secondMsg) - return msg.replace('{hour}', hour.toString()).replace('{minute}', minute.toString()).replace('{second}', second.toString()) + const result = msg.replace('{hour}', hour.toString()) + .replace('{minute}', minute.toString()) + .replace('{second}', second.toString()) + return prefix + result } /** @@ -233,4 +238,4 @@ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { } while (cursor.getTime() < dateEnd.getTime()) isSameDay(cursor, dateEnd) && dates.push(formatTime(dateEnd, format)) return dates -} \ No newline at end of file +} From 4f9f55fe89de432871be1f1be02a22ae24b17ecc Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 10 Apr 2023 16:20:43 +0800 Subject: [PATCH 133/168] Fix ring time error --- src/util/time.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/util/time.ts b/src/util/time.ts index 9954cbe47..4e6edacef 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -59,6 +59,8 @@ export function formatTime(time: Date | string | number, cFormat?: string) { * Format milliseconds for display */ export function formatPeriod(milliseconds: number, message: { hourMsg: string, minuteMsg: string, secondMsg: string }): string { + const prefix = milliseconds < 0 ? '-' : '' + milliseconds = Math.abs(milliseconds) const { hourMsg, minuteMsg, secondMsg } = message const seconds = Math.floor(milliseconds / 1000) const hour = Math.floor(seconds / 3600) @@ -67,7 +69,10 @@ export function formatPeriod(milliseconds: number, message: { hourMsg: string, m let msg = hourMsg hour === 0 && (msg = minuteMsg) && minute === 0 && (msg = secondMsg) - return msg.replace('{hour}', hour.toString()).replace('{minute}', minute.toString()).replace('{second}', second.toString()) + const result = msg.replace('{hour}', hour.toString()) + .replace('{minute}', minute.toString()) + .replace('{second}', second.toString()) + return prefix + result } /** @@ -233,4 +238,4 @@ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { } while (cursor.getTime() < dateEnd.getTime()) isSameDay(cursor, dateEnd) && dates.push(formatTime(dateEnd, format)) return dates -} \ No newline at end of file +} From 6a587876d75ba23cce24653d19012330021c6521 Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 10 Apr 2023 16:21:46 +0800 Subject: [PATCH 134/168] v1.7.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f8dcac325..6c1227526 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.0", + "version": "1.7.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -63,4 +63,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} From bdc2413c3c72c9642df464f8c20cef1efd5e31a0 Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 10 Apr 2023 16:21:46 +0800 Subject: [PATCH 135/168] v1.7.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f8dcac325..6c1227526 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.0", + "version": "1.7.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -63,4 +63,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} From 691d55dbaca2f3e4c698ad453c4c38b1533634aa Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 11 Apr 2023 23:11:47 +0800 Subject: [PATCH 136/168] Fix error while jumping from report page --- src/app/components/analysis/components/summary/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/analysis/components/summary/index.ts b/src/app/components/analysis/components/summary/index.ts index 6ad21f20c..d47d9ae2e 100644 --- a/src/app/components/analysis/components/summary/index.ts +++ b/src/app/components/analysis/components/summary/index.ts @@ -28,7 +28,7 @@ function computeSummary(site: timer.site.SiteKey, rows: timer.stat.Row[]): Summa const summary: Summary = { focus: 0, visit: 0, day: 0 } summary.firstDay = rows?.[0]?.date - rows.forEach(({ focus, time: visit }) => { + rows?.forEach(({ focus, time: visit }) => { summary.focus += focus summary.visit += visit focus && (summary.day += 1) From 543228ef99ede919e50389c62b58c04f011bbe30 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 11 Apr 2023 23:11:47 +0800 Subject: [PATCH 137/168] Fix error while jumping from report page --- src/app/components/analysis/components/summary/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/analysis/components/summary/index.ts b/src/app/components/analysis/components/summary/index.ts index 6ad21f20c..d47d9ae2e 100644 --- a/src/app/components/analysis/components/summary/index.ts +++ b/src/app/components/analysis/components/summary/index.ts @@ -28,7 +28,7 @@ function computeSummary(site: timer.site.SiteKey, rows: timer.stat.Row[]): Summa const summary: Summary = { focus: 0, visit: 0, day: 0 } summary.firstDay = rows?.[0]?.date - rows.forEach(({ focus, time: visit }) => { + rows?.forEach(({ focus, time: visit }) => { summary.focus += focus summary.visit += visit focus && (summary.day += 1) From 86d96ee974a608afcc4ddbb94c32629c427fd1a5 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 11 Apr 2023 23:15:24 +0800 Subject: [PATCH 138/168] v1.7.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c1227526..d60e82d93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.1", + "version": "1.7.2", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -63,4 +63,4 @@ "engines": { "node": ">=16" } -} +} \ No newline at end of file From 08fe91966c1e284664dd0e75490a9bff60e0e81f Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 11 Apr 2023 23:15:24 +0800 Subject: [PATCH 139/168] v1.7.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c1227526..d60e82d93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.1", + "version": "1.7.2", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -63,4 +63,4 @@ "engines": { "node": ">=16" } -} +} \ No newline at end of file From f64e65240b55448ba959f979921367658c0d4617 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 4 May 2023 00:31:49 +0800 Subject: [PATCH 140/168] Optimize guide page (#205) --- README.md | 2 +- package.json | 8 +- public/images/guide/beating.gif | Bin 0 -> 13143 bytes public/images/guide/home.png | Bin 0 -> 18375 bytes public/images/guide/pin.png | Bin 0 -> 19029 bytes src/api/chrome/runtime.ts | 9 + src/api/chrome/tab.ts | 35 ++++ src/app/components/limit/modify/form/url.ts | 5 +- .../option/components/backup/index.ts | 3 +- .../components/rule-merge/components/item.ts | 26 ++- src/app/layout/menu.ts | 16 +- src/app/router/constants.ts | 9 +- src/app/router/index.ts | 6 +- src/background/browser-action-menu-manager.ts | 5 +- src/element-ui/table.ts | 19 ++ src/guide/component/app.ts | 35 ++++ src/guide/component/backup.ts | 53 +++++ src/guide/component/common.ts | 48 ----- src/guide/component/common/article.sass | 72 +++++++ src/guide/component/common/article.ts | 58 ++++++ src/guide/component/common/util.ts | 78 ++++++++ src/guide/component/home/download-button.ts | 56 ++++++ src/guide/component/home/home.sass | 45 +++++ src/guide/component/home/index.ts | 28 +++ src/guide/component/home/start-button.ts | 25 +++ src/guide/component/limit.ts | 37 ++++ src/guide/component/merge/index.ts | 50 +++++ src/guide/component/merge/merge.sass | 8 + src/guide/component/merge/rule-tag.ts | 19 ++ src/guide/component/merge/source-table.ts | 56 ++++++ src/guide/component/merge/target-table.ts | 146 ++++++++++++++ src/guide/component/privacy.ts | 23 --- src/guide/component/privacy/index.ts | 72 +++++++ src/guide/component/privacy/privacy.sass | 10 + src/guide/component/profile.ts | 32 --- src/guide/component/start.ts | 33 ++++ src/guide/component/usage.ts | 72 ------- src/guide/component/virtual.ts | 43 ++++ src/guide/guide.d.ts | 14 -- src/guide/index.ts | 27 +-- src/guide/layout/content.ts | 34 ---- src/guide/layout/header/dark-switch.ts | 38 ++++ src/guide/layout/header/header.sass | 71 +++++++ src/guide/layout/header/icon-button.ts | 33 ++++ src/guide/layout/header/index.ts | 50 +++++ src/guide/layout/header/locale-select.ts | 70 +++++++ src/guide/layout/header/svg-icon.ts | 22 +++ src/guide/layout/header/svg.ts | 10 + src/guide/layout/index.ts | 34 ++-- src/guide/layout/menu.ts | 173 +++++++--------- src/guide/router/constants.ts | 16 ++ src/guide/router/index.ts | 64 ++++++ src/guide/style/dark-theme.sass | 3 +- src/guide/style/index.sass | 50 +++-- src/guide/style/light-theme.sass | 1 + src/guide/util.ts | 3 - src/i18n/chrome/message.ts | 3 +- src/i18n/message/app/index.ts | 8 +- src/i18n/message/app/menu.ts | 4 +- src/i18n/message/app/merge-rule.ts | 12 +- src/i18n/message/app/option.ts | 25 +-- src/i18n/message/common/base.ts | 4 +- src/i18n/message/common/merge.ts | 42 ++++ src/i18n/message/common/meta.ts | 11 ++ src/i18n/message/guide/app.ts | 49 +++++ src/i18n/message/guide/backup.ts | 109 +++++++++++ src/i18n/message/guide/home.ts | 40 ++++ src/i18n/message/guide/index.ts | 68 +++++-- src/i18n/message/guide/layout.ts | 92 +++------ src/i18n/message/guide/limit.ts | 69 +++++++ src/i18n/message/guide/merge.ts | 184 ++++++++++++++++++ src/i18n/message/guide/privacy.ts | 154 ++++++++++++--- src/i18n/message/guide/profile.ts | 35 ---- src/i18n/message/guide/start.ts | 71 +++++++ src/i18n/message/guide/usage.ts | 181 ----------------- src/i18n/message/guide/virtual.ts | 69 +++++++ src/manifest.ts | 2 +- src/package.ts | 5 + src/service/components/host-merge-ruler.ts | 13 +- src/util/constant/url.ts | 10 +- src/util/dark-mode.ts | 6 +- src/util/merge.ts | 27 +++ 82 files changed, 2475 insertions(+), 773 deletions(-) create mode 100644 public/images/guide/beating.gif create mode 100644 public/images/guide/home.png create mode 100644 public/images/guide/pin.png create mode 100644 src/element-ui/table.ts create mode 100644 src/guide/component/app.ts create mode 100644 src/guide/component/backup.ts delete mode 100644 src/guide/component/common.ts create mode 100644 src/guide/component/common/article.sass create mode 100644 src/guide/component/common/article.ts create mode 100644 src/guide/component/common/util.ts create mode 100644 src/guide/component/home/download-button.ts create mode 100644 src/guide/component/home/home.sass create mode 100644 src/guide/component/home/index.ts create mode 100644 src/guide/component/home/start-button.ts create mode 100644 src/guide/component/limit.ts create mode 100644 src/guide/component/merge/index.ts create mode 100644 src/guide/component/merge/merge.sass create mode 100644 src/guide/component/merge/rule-tag.ts create mode 100644 src/guide/component/merge/source-table.ts create mode 100644 src/guide/component/merge/target-table.ts delete mode 100644 src/guide/component/privacy.ts create mode 100644 src/guide/component/privacy/index.ts create mode 100644 src/guide/component/privacy/privacy.sass delete mode 100644 src/guide/component/profile.ts create mode 100644 src/guide/component/start.ts delete mode 100644 src/guide/component/usage.ts create mode 100644 src/guide/component/virtual.ts delete mode 100644 src/guide/guide.d.ts delete mode 100644 src/guide/layout/content.ts create mode 100644 src/guide/layout/header/dark-switch.ts create mode 100644 src/guide/layout/header/header.sass create mode 100644 src/guide/layout/header/icon-button.ts create mode 100644 src/guide/layout/header/index.ts create mode 100644 src/guide/layout/header/locale-select.ts create mode 100644 src/guide/layout/header/svg-icon.ts create mode 100644 src/guide/layout/header/svg.ts create mode 100644 src/guide/router/constants.ts create mode 100644 src/guide/router/index.ts delete mode 100644 src/guide/util.ts create mode 100644 src/i18n/message/common/merge.ts create mode 100644 src/i18n/message/guide/app.ts create mode 100644 src/i18n/message/guide/backup.ts create mode 100644 src/i18n/message/guide/home.ts create mode 100644 src/i18n/message/guide/limit.ts create mode 100644 src/i18n/message/guide/merge.ts delete mode 100644 src/i18n/message/guide/profile.ts create mode 100644 src/i18n/message/guide/start.ts delete mode 100644 src/i18n/message/guide/usage.ts create mode 100644 src/i18n/message/guide/virtual.ts create mode 100644 src/util/merge.ts diff --git a/README.md b/README.md index 947320ca8..17042beb2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Timer can - support to set the maximum time for browsing a specific website each day to help you keey away from time wasting. - support to export files formatted _.csv_ and _.json_. - support dark mode. -- support to sync data with Github Gist across serveral browser clients. +- support to sync data with GitHub Gist across serveral browser clients. ## Install diff --git a/package.json b/package.json index d60e82d93..c43177fe7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "test": "jest --env=jsdom", "test-c": "jest --coverage --env=jsdom" }, - "author": "zhy", + "author": { + "name": "zhy", + "email": "returnzhy1996@outlook.com", + "url": "https://www.github.com/sheepzh" + }, "license": "MIT", "devDependencies": { "@crowdin/crowdin-api-client": "^1.22.1", @@ -37,7 +41,7 @@ "node-sass": "^8.0.0", "sass-loader": "^13.2.1", "style-loader": "^3.3.2", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2", diff --git a/public/images/guide/beating.gif b/public/images/guide/beating.gif new file mode 100644 index 0000000000000000000000000000000000000000..2749cf82e97a39d0e95b89ddc09b263a19115898 GIT binary patch literal 13143 zcmd^Fdsq}#mcKL&bo1<{n}>kdKmjd2Xh8{yIy53splBnOnn)rTMZk)JnK2o&Bk!k( zB|ZY`D4-&iF##+I$;Od~L{yTPNVG5{qE03n&1e$M_$aKsx4MyHKX50r^Udyl`}q%_ zx9i+<&-tBme&^g99TOG$d@=?+z~9DA0RUhC-~f;SPyjFhumEtv|6&-xF+gH~!T^H- z76Y8{Qw#?<4oDnOIACzV;(!yr$4G#afFuD$0)_-E2{=J0Mgg1xBn2o6Fce@ZzzI8H z48R#cGJs+L!vK~6oUlE{0-OaT3n&&aEMQr{2|8gMz&Sv2fZ_nd0hR+CQk1{|fdhgB z1O*5N5G)`#Kmd%uFapO25+f*#U@(Hk2o56vPGC5J;{=Hl6izTW!Qupm6M!TzlE6uV zBngTn7?NN~f+Gn)5g0|_6hTr1MG*`|uoS^j1Yih^A#jEu8G>R6h9Ovn;1~k11jZ6L zOOPx`{sEK6`K0XPEV2%IBGj-WV#;Ru!^IF0~-#sG~2nglcjXa>+MpgBMTjK(k; z$7m9xDU46lGq%o4lNtz^SiliBmW=WbOX+Y5! zMdK7rQZz-;3`MgP%~3R9XpEt8h9()BVrYh;S%&5q8n86R(l|?#EKRXA!_q8Eb1V%w z8sli3qe+gYIGW*TmZLe227t!^j{}|rJOy|L@GRgtzyplOFdoNv65}b1XE2_{cn;$M z&SN-_<2;G;6wWg^&*D6X^MK?rlE+D&BzcPD8Ios7o+Eic@fgM96i-q-Mez*9vlP!! zJYaZ?;c%s!vg>r53&Z_@w@w4b7e};sdz?m zdy#BFxjmHfKD9|PHssWU^WQ{l()h1Ste@g_X0wNKgU1ruY5dX&e9WFI$J&_-D%Qm= zm|D_zVwY>y<*#b-1>XB{-%fQ}X409QUqWKMFEpvgESP){mu%R&Fn+x5PVb{H%Wq)w zlq)ZOFMe;I-?ztiuSz&KW9nOXeo zmD4M4R?p6>dUSksOXITkJbvoRSHBIs@bNZnaGKjCojN;9-jUfIb#`CN+d8`_)f*}| zL;h8*#BjzwyMB)@I8u^*^!kNr5R`V4{js|4UH=_!Q{};$A^+IfBs6F!O}f;M(BL+sG?p)`nvj+uMShy z3^)KcC?E=$$vp78rZbSL6QZoL!D7k8d>Wjmr1& zPr26O*cww|)hIvkhVH9#B|*N0wuf~_>zR&)e2A>z1x2e}*HnjtC-#!_2cYOY)-!fS z@zLt=^{TN!q^n}cc1qv~Wbhx5jR9tyaH0%Lm1Fk-YHxcH@R((L-?WEtegq&H3{oI_ z9`13^-)Lz`{Xs+Zi9&^)-vX`t>TK|XclyN2wEf|vm0SXTPhqDFe|BvCsipDN=q&>h ziLkezu??^UPQZE47}_+W-)7_KH4%Wf4cO4qpouK{tNNvnpfOGtRsE*aqeSPOP1?j& zB&I38?Z}uI*!0+Soo{TMM`roGH>MbkAt)*XQs6Qu`oW;1Dq_1DyuxF^U!P~bYpCtq z*zcx=4b26n&#y3h$~ zJrlVw6uHns8IUN;Rzs93V{=pQBy|?VTE>=iHD1@pc{Z9UZZ;k`Q{L<8rmp-61B+~e z|A?Itdrk(e>LP$*75wY2e@2oKzm-(AzR1=mmh)}YOju;&wWu$Zx*k@pnU*^-HPGvD z-hu!>@58!paS-#0L}OL`&77E^*VmlNkNd7YV`lWm?z9Vs+6hTsQ1|vT^RtSoZN2!B7oj z&lEfRq%8P-)4_9Z>s~#(XH4VHkF&uLsN{msG0;SzMiVUyHwtaI8eXKZXxfA~LhHM# zlKa^6<4!}N<*}(cWc~RY=AH>!(OfDn3bw!;W;BG7sZf~o?ph;C)Hl|mtl$&7C)H5V zo~sT{@9WC+ipr^aA*I_-aOIpwHaEqNlR>z-K)3^0u<@wXrlSh!1q#BI`UCk^6Vlb% z^yC&JCKe=x6#h-IF<%@qBULE@&m%1eX#7taFIhNQMEsnU93D%3T{ zR^`Z8{aDkv$z}>XG|Us5m0=+skp{=;1^3-z*ZD74akgU8tImBrQ+ChI>m|SG8?W0F zy-4!A->B1n{Kin#%f8IHh*E$Z3~41yY(O>z%rSGtjX~|az1HB>QK<0pgK2kp`&{Qs zFP0S_U0%O=tW9BGsKO^EZ``mF4HJ(END)Sj2Z8j|y3psv6_eBZv<+iV=O(zO%&JdY z-n_-!VbP57pI?a}T_S2Q?YrKGtK5baCu9j{A?3s%x!SufrsOWjb%8}8Gr6|ZHCS@@ zQieBJFF9!Fsc87)$GQFqbfR{hSMv^C| z`A=47XWRk}O^fI6ZpEdyzMP;{v}%EJdg+l>YpeBf_qa^8_S1c{Go5>n9{H}K2G&ez z%DL_G?49K=En5FudFwL$P^^x6P!&9nB|H(3Il%L%D?ho?3R!{M3~?wpLJck8uYk-R zOfx~pYFdf4?C+581m&9nE5TmDE6ssMl}(E6h1h_lP-|V_{)4q9KwWRAKUpA`yV;61 zLGZN_e(QliQv4ds5t3l8U=9+?NWtZY6xKK>M}WXKQn+r0H&<;cg?;u;KBSP5oFY0474>e9{iO|N~Kln zvn*BCw-22+{Yu!x=o7?8kpDa?kxyiDzZnnKUTw{g9hZy!hkPv)xQhxLdzykEgKMa+ z7yHHKX+(^DDifwf?vNn~@(&;T9_({XM83FsjAVmR!%Wwh%jP@vwO9}%umH~|9asgeerCU7?S{^oKY~ufJX6^K63y(IJeVI9}<7h@{83sGOEMYn?E#L z^0zzt>T}E^w@UPPu7?%vy7ja(m=Kn!h77N7C}YHk!2% zfRKU@IvY}OnckzL$YvmXkJgaz_h8u=^awPrP!1%q{jIpA%FW}TLVvQ*{MIzJlc;bZ z?rH^z9?Cww0&2qZYHc6c7!!kFRu@Rr24irrtOzGTX0;I%hDcf=dD=7F2=uBeP{_+6 zMe&|BNt}JU+rHjEd+L;(+ODTKQn@`*ORzjdSKVC{c4ct=%xq=o;BqK1{|6)Ic- zLgKG+N6$$$iFM>J0N-Bu+RL+n?c=nYr)kgWCH%$v7qIvXT5z8`EY+1m$_r-4{G0Z5 z&ybSJph(9D8P{gN=6EaS-gBRey)>$^P_#$|5Icy{rsptrr+^V)C6sYEFMy>=XtK;! zB*-+|1}mZ|AXKIlj3Q5}pML?Cd(=eFt8Ob$SpAY6cPeCl zO+=PGqS6@}5D$W!dS;s{qA9hiOaZF`m&{x%)2hQanDb*2TMU|Pahi98=dv*A4C|n* z)j2%(w!Nh5w}zgcQ&ErTgs*jdb+howW5Q&9U2WQZGa!i>ObpfN(G5#K zRUl%p;Mm}2J^L6VeKD%3{$-+u@~K)^=)SrBv!u|t0e17! zXhqG*%w0_@gS6JIVOjgFy*;K`>t}-f+{&KW@w>w_Z)-13Xql%zQBd13rn~3Ietqss z_UXN;=dXkVagiG7^ui&Pu!$P1UdR6~GCHcLkrqa^@Td!dR>mkmuv2yuYWf`skzwJCdA`Gpa?xf!Gq{NtVoDjT0EN5rSm$Gcl+Y~)iTXjt>S1i zG6e)TXE1=^meibU!Fxe|S<{ib%TgTWL!KIk@_@HN(+tDFy9bTYa;lnU?rCXAt}A+? z5D!s)l|}w#nEUlwA$mOsl4D|!pl~(zf(n!U5KoYtmx^QB5eEL?*Ehuk2`vnzRs+fi zxz`6%>!<)3b>V;BfI}FGL>R#gnh^5p#ry6{8uo^*w=hfHw1nCbmUG1KHyb(gwE@)FS-xP94phtW^cUaol>i%G;=`k_+NfD&0_w+V>!a%-JsQn>pn7oS|FLZHpyzszN*xauVGk47RRhjl-+)%vOvFkR$}B1!P{=A z8bynhS3sI=1*g9$u;^oaleUvXbFFcs$b_|VN;cQS56Y;uaWfs zUkvw=)ct3BZlpa%HHol*eC;4i$CjWD4wlq561IvfzI(6cWW2RcsoQD#m6OHF;IuF9 zt^H$Nj;lq=>idDJb_z6=G_9KN8=HHX+Z_Bb=)#9Zto~2Z>PZVvWsI@=i&T@oYStt# z{$cR0{~iGws!_ri0TR6MTuTQu(nxME+@&5~a>hVtp+GdYXv27xanuCO4VzpTab0U5 z@+6PFhEGqBB~TUBqMzUyNiDgS%7BH%uhqj>zZEEKG)*b^U~^)q|O{M$526tTbKymfLWKc8fpZjlGa6Hs*hNp zR1oU~q6M+x;F`U2w;x@-GnW_N0vY{?w1?MmkO>3Gge8xU&I&c4`;>8yHWlmC@Umda zOWkkkE_5yvYx1i=^FNQ+k<=BmGE#430@0{U6)<~A##KFOGi{i&jZ_9@A~@NQNvqLV z%`##vR|NSI##xG>0&2O;1*ckr#{xqd z?6jU8g`=Knq=kRBCg@6) z!Ji+o((kD!rAlXyZ|5gPmROx8RxG~b6$RhGq|%GGzEZbEaxCqq=!(7C?UENCUcf%R z5?TqSzPta@TJ=0I<5tNg+0Wn3$%&3ncWhDrrE?z;LmG|6?SyfrMhHlSJ_HO{2P7`J ztBJ_vJ!9YTZ@i{oZ!_?sIEo!%&9xL7P}I~W34N2rEqP**uEH-*jCx0SYIEV#@S*|S z@K@-k55}k9N>qZ3!&EejTj8q!Cr}ZD*WsSo8|4vG7Rutq<}rq|1vDZ`Fw*nre$D*W z7_n+fK{Zs=ltys$x6VaPtxP}1XlK*DPH1_g1{B7NyaG25k2Im^4m~nz|HK`3AKy!t!x3owx4gUnBp%iQ-xB-nc zjJEO>I=Y|ylOdMY@PC~ z9jrYCo1A~hk(T_}kLA7=Yi5YidABbMblF+9q(XBzry|O&Y`Jb# z+b)}fuJ6dy+1}%y`$}0hmD+PEch>mtF>PeUXSus%DQ90Y{JAHGl@+cIZBFt7R~rf> zlwIrPy;i-So3}c(zL={sd(*f0=tQvow`O;5UHa??L;g$F%|!Rk+wC8Oxvzg`y5T^5 z&L*>keM@!T(+_=b|CcXcQ2g{^?!9lKcBb#VccEavefo#H8*9SewiPGKkFgINPDl=6^e5b(nl)9KDsRI7ILg?TAJhV2va~&C_kXCy~j_d%HAkB zCSTzXIdqmp`NAGqc7N-C-re{Cq)>O^4WUOkc*i~<5jUtK3NJg0yN;pg^@Xyfnt4er zIWm>~ZU2nzvkG`m>lOZu-e9hLnpe-C=hqC&M#kD}3$=p%{$P@@dNijpL*4N^Nv~bE zRPWufNiz6o!ekb7F^f18%oQ>ZS7H93YHoTvqxxk2fV6z|n+o??n-AC(pT-Wm zc~+EvIsCQ~-4RHH+HMOTZFaUv7HatJ!5sbqk>S%Y z;U&E+vc{#D=Hfq6R<>w^}k%L5WhyWtb;+8vTt_j>rp^vA86 zIO&P|_jmT4taSgO*ZX*+8y;UH64{{gdxMpL=+GLdbi&n3e52(~`7gWmk$S zl)kYjtG3|}2QLM#3C$e1(fLW?BoA;BOnvz8Z~wSBw(;_#v0omDNxZmmVCo-UwrM@$ z{Qgv*Rql)5+j}y;s-ZmnNF02`xZzuCzkfTwtZweLsYieIcRo@V5Aa6w)-_+RXgm2$ ztn-}33+p!g62-f}|L?DmWKGZqV^i7Vm*yVp{PZGYy{0*S@9CVxKmN6G@1HYRpXk^7 zKG@g%yN?DMy(j!3_13$M$KL9Uou62jNqz3KC?vNx`uwVUCA0chZ#DQUa@|ns|<)5U6ZOcVz0-gqp6&|%^7>@(9(Mec1;OoDRe0#G8ruS!4F1uH1_eU41 z!d~ganyODs@(nKB{vh+!*rNR(r@FbuB-9m8UM9)bHtcK7Fke;oPNZRXbo_HS?aHF= zj*HqaiTuMoxn_6U4)21Er|OPPSd{zRBynSnx?dNeE1w`7aPW2G1L578MM#+}b52`c zt;ZMpyW%y?<(i1~J*UJ^7^MQ4M$fXGL6(Vl2{a!0#B%T*f`AD*>^7X?&rS6RGH=t% zHmd(5yzYY6aZ|2-SSmFo7$Kk$j68&FvbgknrE! ze_`h#t>}W_PP5Mny>)9fbr13JMBcRz^Y%3JMzhah)K+eppVE96(S|F!w478j>HwzS+m= zxx3}<_odZ`CGh*y+}+yt%l_$mprZZW>HF@<`^SCn5 z`^NvX{D=8JIe7<7z6{JieHcMgFKc^mAIgUeEIfSx`e&a%^uF2W{^|chgprU9Oh5OH zy{0fS{h*^S-~|jWJk>V5|Ni~{p_p0i#?#O~7lvL$nycGX2H+7S<_>lDyyySEO=D)( zu&FKO7ksZyN@idRAtdhUe-9!c`cD~r55y<@U?{D4F6HM}x2gJ7@dUK0>=}DEx87B^ zuIT~2m+}dGI6fCgboIP{FqiQ0zt^VC1^WCo{dOIs^}FJ|giqju;REx5{9XB0!Y_E{ zucc+(ve(*JUh(|VtE(Wb4{?BjcZ5ZFuSx!p0eyL(-s(dCZ20$+bZO<)g~QwX{|f(T zY2VzFCU9$X>3Q()(?`x%ftn#iBonJIA6~an`qS$#AF-75Q+AHuy1U=!ww@2q-@%9P zJ>&oN+ztxn@Am7-<@@>7f2k(@I*p=dS=)OxG2gkldtcst?H+!-e|(RMKOG!;ADw=m zS^1}D2;Mz-zKt%(T;F=UyLoIG_;=;6{?Jr#ySjK+ zmGW<5pt}0KXY8=C@%`@d=B~H)ZU=m~Fu}lWbvN90cXrXy`93yzzp(UlcXYJ2a(>rQ z{!$uc?O9&iavc=kE9ECKDls%kH{?ysJj<*bf0;3ug#1BhS z<#_aBZJK5?lKh!@vchWXJ<_7>!hFeT(@WQ@)9PI;qhmCs3_q5>ySti_I@ALq7y214 z(LG8|9VCG|jCDAc(z7)2>yhndr%bhxh2WCrDPw`$Oz_mRt2Q?^-iGLH&u=x3*Tbf_ zfBu%7*Qa7fTOMrPia87lE z%QrRTvI#l8EhdVkY=TB}O~kZ>&rg(;Yb9A!NTL5~WC0dI?hH32TW|BBiz}_r;og zN35_Hwo`hhHuKo%Asf0DRR|<%qS+z|0=?p+J?89RIZuQKVQOMz7o)yG3DrNYv3i{s zj`omf@;YqYJE&WTCHpT-c}epV9~8_8Q=0b}x~>T7Zc^!GWMSUKxGn+QYI;I^nG*t2h%yDQ&5 z;N7I5aLB5R8u!a5r*l6i=;oO(xN%*^i(cy+?ZBmR^aDCvsK3^s>(_3hT%phygL(Q( zo745#>3Z(@$o#3z$V?{OnQ9Lmk0D^M{fh)t)X?95!hs(2iQ~boSa@R`!W5N$JQ$8w zSw>E=>0eIC>9nl?*s3Ja9Qjb9zMr)NwLv#elL6!5EVfM{?d$@uY1Z6z!LlK5;|k)B z?QP_FwO)oaB9{FTMRz>*yoy9dP@$wp3%L|O3WyC^GY)(N=dJpiMZ$Oh&`Qu>h7Tk9 zh*HjtuBSa_Z#v)|)#{MoP(A^& zNe=7bKsJ>zgV&;lUSr_7;m&g;lA&sME!r!#Xy}eaII=rJC6w8lEe|`n*UU|qTsr5n zQ*9qsp~pg)3K%Owo+s5_ICaI)SEkN=)`_h1A&ybMlye{xsBk&Vt>4u*ro8^ zHPW(sPTTB4IBz`TYv=gIl2`Ei{KI@AIL&a9ZtC(8hAO=9%2XnW zuB_jkj=J9WWi6mda4$kZ>OV)-M(GZ|bo(^|Oft^@;^;3VdmDo2zN#_dO#Z!28|93sSWiKKPRMF8w#|^etTNJZ zPMp{P#l+w^Ey_TXu%OVPN|?$oi^Sw#~?Qhmi6EcdDdOSR^4W~)33F8ma-MypCD7$ zlplPC+CyI_h@Hn#wMp9yMcdo9UHIWet#kR}LGdB>A(5|3n%jnO9f4c2)X1f80TzK3 zg>BZSoLkPTcpJxbSW(oL$)%)*Va~3*(3a&lw=EMlrX36w^Q1&|&5H8DHKe-Ep@(2m zlr=>{E52^Sko|mDEwZ2PfIR4cwqX28w!lL|>Vo53D^XD8W}1zEx=@~&2iEWE4KDf& zO7+iLy4O+%_pOy?C+4@;mMC$G*iV~0+sNHRi~lZM{a&HAvn+bR`A#;cu&(55p;5v8 zE=Soe1N_|u)8y_QjXTpQzUhw_jYs9W_uk^<RWACuc#6Z*XA@pE7C}yR;g%810-SY%*4w zn=AK?QovP2Goc@DQ?XR-=3Rg&QonrXrcGS1PIFE))yZq@RuWnOOC{1|H1XwLPkl#U zjugk)*@b*tSPSKXCEmk1C}bcMI+pqgInYJN@42FWh@ z?T&Mp62sfUJ$)R0fRto+gqLYGXnephr-qgirNJ@W3#91p;K9M=RQ%atBd~QXpU<_I z8XK3e3tQTz(DxA=jur{|xpoL?b4iVS%nks}YK}t|7PuAI@D%BskhNad85%qJI zst%RGO1x_=(p1UMCE>fJbL{>mA91QzZeOZliADA+)AdTGJQw&4tkyP1dk!TtI|r8U zQ-PCmASy1-`qF=L7xe4q3vjrod&WeWSN$M%RyHhDHU%$VD`iOaQWc$|LL5<%ZCsU# z+C5BlF~-!H@e;g2;n2fH+DQjkI5dE5&kq7PtBAB)E$-9grXqFYK(c#EIy2TcI5zs_ z;Q?)1{T#~C-+@@gEdW;jhU;SjS))!Gu_&tM0LnxwPw3g=I zO^{8jWzlpvE}~$Q@e-TklQJ8lH^o?R*w13d4a0+m=0hr3T~=91P0Ybl8#MD-1?Mge z9wEt!T$R47`vlP zu9N>rQ_&=Vy!%Jwx_P>rsyam2h1JzL*D01!zJJFbI9N84#;_6N5|&{5^WX6i1oF%f z(MsA7yYi(EtzN_-g$uGuYjjteRilwx(*?_V#)9mJd7=5e?4*qGXUI4OU_m@F0haDP zo5d=2jyMr|RYM5&+i7|CAP&?*PT)N8%eMQ$s&Uuw_#;zvcQ3zPJIe? zwyK!;R~E>&ibGxt1yixF3 zuxn$n)!3+we|@vNLOWf~suigAn|f;D9V1?84Btybr0u)C|*;K!kBQ6sX zAZV`9N8PqXKPR!Kj+5mH9^2$?>(+`t+@~`?s^K{LcN7oog+nW`8C@a*{>*zGX|o?(O-~D ziO!6juV?6<8dP~Ip*sh(ObB_A+;k_{Afo7HZj8G#ID;}mxfyHqrv77Sj^GNulFscg^n&6|Y76KmQ!{tL?ut`)^Bzr_wN{JDAh zfHd2IPa2dn{(GQ2jTNrXjagvz{Yz=Fam}Z3RqG&G8uxTg-!v$@&mt^t_|>O^L3$n3 za}v|cI!KKXOsILRZLM&*yvB~2Wthvv3|lym73j^CkwfLOia>stvs8>PdVPoYb*2o3 z-gV}g5Qns2WgVbfMh^lmVn*;Z9wfpbb@uld3-lq3vjy)h7fEA;67s_t^yN?aES0XG z#c;W>ecE?QbjX-;eS*-rzBp2p<4n+pP<6njlsB0+Wv0w~;SfXuWwmU3+8QPle~lvL)-*PYd+a= z0IqASCKaJ9_LvyP;DD!jz?{<)soT<_y=#exw%3@tls2iH^zwP(6~P1&#w#3}&I;b-=lj?P9Z)PtvfRa%e^u2c`R02b*Twq;)r{K0rF zHl=^GORuIoXzGHDuUlsB<@7OD9b?X+y=h!RhX23=z4b=uR=ELM>K7R+-$O5hV*w7& z{ipmSBX$srU}-mmzhq%yJ>_{QMuXTyApwWfpu5ZM9<%@#C-_0^hoV07mo7r~$fq(e zY7-=+3HxELZ%-8&vl@$l?g8fXc#|0h5lUegnG|?E#a$Ml+9pouv(~qkYG^k13D5W% zu*FoCoqsCtaeUNaAklKM=b-Ei60PAbLV?eW!;pMyFpKt~i|gH#gBTR3Yj~i%Tjm9g{OtAy=Y%+65ppJ&l zb5(w84e+!O?)Z*NhE_Lb{?=oL#dIn%t#RFzzDhu7H4_2!`RkVp{wRTXI$3xzPxEHg zjsrU!pQL9H8H0A-s4`85(5kgpVn%pjG<{2F7fsii`kD5N7mP0X4kG3t_v#)xYtTn7jy8VKqO z2HK8rJh}_I@$npXCUYq5u$n&{`(18$N$?{+G#AEx;da;QVVoB0s&gL1_h(a$F-(kT zW}B^CB@;yJql)uTy@Q?<_=bgf1fsT6Reb2A$M>ax)c%O1XTIqbD7LTBl0D`?q2aXV-gbpJ2kPoxnALymc3^ekl z=7Gz9C|Jdxb|Z{+Usg{3Mn$o@5LD0p`BW=Bws6f;<}&b+g+pBIjZ@IQ@B(QatFX2PgD8b3FifE-xY$^0Ak`O3`70 zwMpW1_Katpn{#%IdA698V_Pp9*=9`|xn`5A>*RHzfT%4XXq?viXF9Zr7x{Dy2yZO9tRsRQcMC^RvoVn?dm%9&`I ztZ^dvNzDtkWK3WtXP|#kd@Q4eCfKnE#LXrV5+CHJH&BVWlcu;G+*McacrklwsL6s~ zt|!-fO%SI*gk|pp?0}UjxYCaIk}(AY5+0Hh$>rv$9{Fmm4xq?ned5_2O8XVP0P}7> zmmTKD{PixhkHJyowJ^=*)4Bn+eR))g&P;viECjAx(&ESv@xAnUVOKiS6r z?N4op&$DhFyI{tq)x|~{hCr3Fyj!bs>j#j67aWp>SZ5|2)l%)h7DVbj!l2EtE7&EG zfxYE#b>LBKWrE?TT*&vVdOKLBd6}cOP|D?Ub_ionxtf%ENt_;Y@%Qm|<$U9O<@Oa{ z)0W*`36;)e7WHThVckh7q$Q=Tn8|AqB}YcUHN*NAwG<9AG`X><(aKMl>!+GQ+zyt% zh%kq}Xesj-cQ;RNeSpWs!FaKuHa5t1M)Q7&f+SxA{_u+7fEy-oG7|X?LpGFR{tHW7 zrIwTX3d%#nvOftO__quIb(yb!IeGdNem1VC<;*c!*0z*}x?FH|4xqueWW#J+6vItO zdPnLL;@NKCMKFp4gr(T^?1ABU6t#@ypQvF0L(@$c79>SoI+gL^yGg;CmyBs6{C+ka z#Em)KiQ`Trg?#cCplF6aWzZu%!a2<(#U?1x13_{EV%(y}b7h4Z`#0kneHr%(nwICn zIavloga>J7#)Bz_mmD5exT|vbktW!L%#Qj^6rJXZL~RJR}w7*s#)zXrxDomoF(LnU4s#+5(1BZ5Y#QfLqvPZ z-BmO6?j-xrmb*Ox4<{I<{*tx$(zO;X57%!G5C6)P+ui%AVZ9Xs{2_In28*a{Qybs& zziV$nv11jxSf;`oKn+#0xB+$!qdkgqV_MM5Ur*6VX#Z&cHQqjQmAx8~0pVwd> zA{l(eZy;X1NOkT%om@Gn?oH5#E&1~s-F(u*=etA@M|d4r5!h3ls}kg(_*l;_R>l~% zcy996Z@qCw^vJHZwptdf>^(vF?tea=SzyAnWn9rF1=KdHQS#%h>#KASamoljI1)Cp z>J|pwI7?#IG%meEu}@S6P2kp*8Ts+{cf-YbCm+Zrbf@D0*P)Dla~VkXQ^RNC-d!m> zZcc9Jr=tXXKACmT@p-VD`CI>KaVGyJIJP!2$Pc3pPx@v38xfAQKzAK68NZ#`1_!UH zb&50NcT;q4!0J)?<|lt%Nl^ zd=#VJ_ESn@EDN-J-+J$=&`Tfr#qr|--2(*Ski~thql*4MIp(eNE?l##+XJv8yLv8g zuZwtC#PtkhVQcDT^@_%EhV9wEZI7o~bA3D~Khq;3^?Rrh)}KdZUe+tGsuWrmlJWkz zJMg2|Z*9-;O;^Ke!L;G=_5{-jll-H6)T@$4EG0|>9-euGWlNy7Z-Csf{h$h$j+cvH zkZx*-#SjYMzP1CU(`O*h>hCb~b_|7)=)qltXTih6xDnXs3d)db2yuOU+t&dak=Ep} zk~Y7I5WPHVdg+zzxfHCgwu432deMJv8PH+#d8-L-;`kbqpTB?U0Y}6q2NwS#!qNX_ z#0p$79UbQq$Ap{QIGunSTP4_07lFJ`=^~^VKrIw|eDk6b*ufMsp1c zp>8XjNDKJc7{q@->IbP8*2nS$DW8Q_{OJ!0a+y&YcMFMrS|goV?GzzG3nJMv zUypL#4>u8F4%rp$Jho`H`uPK@zOi+Imcd1u$XL(CjeNx?Y5Y^#Q<~un(Eu_?u~*xt zJs>~MIhy}y3dSn`{DhAGT(zBEQAF}>nZ%#+<(qg90@3+dj&z+~)A0GDX1uMk?Frx0 zbl|Ks+5o!k20kTN7n;C*WF0n+>=eQ;mKl|1pFg;A9Bl(;>$lS2b(C6~)Y5s*IL&8>haTZF6TuN>n2^3ha<|Zeib7+R2 zxRX_CFIh4-boPYNXR-|3!0b0cB_EuYB<>2_({z&s(HJG9h-dr}q|K0(3>)VfoqXc| zL?Otp=Y_J^jCh7i;)V-xqeT6T?Vwlm_(j%y`bO(kM+D9Q+zhXeBK_%VZD4?b(C_f@ zFdl)s-YEVeX3VACJ2@_}oWDG^1rb`SUS;4|3c=?KErIcC?QiZ=1W3>kTITbT17hzo z4?O)-eA-1px7N+3&p$0NGm$WiVSG1YL?i|V(MhA2UwsY+`Y>@6sKL05e`nbOZAYkE)MEPk$c2DRS3`-+crhbp zv-TYoMKycstofv&EgQ&mS)-kz6SQ75{OQ2KxNBdNG}A)rt0ytV2=rV$ zXo|ce27(r`og-I9wWx=M2&Sa=cdgv!6vUW)k8nh>RUi_qeL;@3wZ{DJf{2}Um{JLn za_#~+m2{b&&2BndlejH1yl{r|IA?G~UN|W#aE+eA)ZtBEd?fwYUgT%;1DP{)@44)p zw^CU5fF*zA2E~(hy=u0y@d%j$5Gu^CZi}oOPdAm9Q(y>a@|{H}kOhO&Vi{z=Xc`^A z%#`kkyZ#|zU8^KA?>wsS+~^pi9P#+V+i|2z`|tZ(XVLgh(ccw2Pe9T@8=zNrB6Tdn zosT3z=Vy8>LlR)OD{`Vx-adwOpiNnvKX}9)@jhVr>yOiQF*ke+HpNf~zhNlhu0M#d zR`G40@(wo0@J)>g|8{g}=nn(}nwg<{oFks~?ek1DLP;@<#iS2?Ufa49A}dPWL=;`P z^Z|UXaT^;Ag%Ci!3vB7;qQAp%l(GJ(VfG$2ooE->j= z!rZM&Hk8lO-7?>W4Hyijta2nj8{U|ygPZqJJ4bi}|CpBf6*7l5W`6#+hhXkc0u&7) zfp?FQ!DOmyge_lA66tG^$T(TYAUr!tx3c&>hUcjQK{|L~?I6(=C+PZzVX=uJxLGei z^l>kRT-kRL8Gr}e{!MKv9^s@++cW-0Fx9GeZl!P>%a_f!ATnQYIZeK={v)=@4gu>6 zubCV-3!bH|;njDh465LBHpZ4>BE0cbt9koE?tRoU!&tJSGZetyT)XvIsA{20k=jy5 z@65u=DR@&^qi3_Z0@k=9?VO4jPd?RZf=jyN=R}P5-jM`PZbG@4jDRofa+(8xx#`v? zLH`Q;FR%KK_@?w}H9mpi3yMCx?j4qHr7w=^jY|f%x!Z ztKf3~AOC_f!chVQd`^k{3zL)cGJzdFH)&oif!TzNNlsvRe5)$$BjAG!0$_(R(>sXZ z%2I-*WboR&g^FPv8wy!^DOhyj@vb*U3Nzs0_?U$z&7WNnnoxba6j@Uqx6?}6Zm`Mx z9Dgw)%q?HHEqx;iIxbr;DyB81zlR757#BzgYhdUR_ssJT0fOh(_E$obzum((h12|7 zo*Gm0Cw7*s`(;X$LHWy#FT&a7#*_FYo0`4!s4>o=*c2;CP;tv<0kHIj|5&mET!>)z zvnB}GOHtn5+$*Gm1r6WaP1b{^xof+pj%*R)(^%w~)h*+2O%Dp#neZq3j76G+xwa^( z6T!AXZwB%j&cS+JY9?qRt|1C(XQDA4souryH2>O&@M1s2%J9q6d)8wmsr$c{-ndt` zs;bwLeCsC68QeY=yh6~#rB8^bGjdk3C;@{Sgt{GmgrIoT2`ZJc$zmfe zy*7o~2>7-rjKHqmd;i|MYf=P*2W~tzLiO)8rqmekV)Cs&qWxw+qxK8!n1ADS{xnP> z%@u6(?Eb|b@6Z{^LYKPO$YwSSDrwys6Z)q7a#Q%}mtAc)hB5shZXvKmWh%{e-<@ah zY*k5KoC@uD3f{D)*?w6)md=**?l%=8QMk_A=sD_^7Jv#VWAv!i&Z6k=tRXakfB932 znQB)mMT$3c*WiF?;$LlJI((sA$ihxDAccVeok+MWC^H~JDG;4e@%3WHFmZyQLz7N) zRS)sr1354foVGZ3p)|fzX@ZYwZAm-v)QrUK+;*_i&IA5irUy8K_c<%%v4~|r=X@fE z!|p@7#Pi$SYSq~u2l{o+Wc|)qoK2=@(NP5S)dO>>YieX+%lHuq09LPZ^JHF`2A*|! zXPZ4J+CQMV+nBd|x~HGH!E0$!3U(ZA+06r9i}s{smeGXQ~f^jKYG*^@0 z(VkHxgB&REo-OY=B4-r6Hh?iH%Y`!Gl*1U(jVr9;Q!#FPR5gGYT~uva9@tzaTzT>{7Xw8E2GTQFd~jSSO#c&Hf`$jc*GP zNs|+VU0kTmz2?O`s^R_gvP!1d-}!Pgp}X3KMXy*Sr}J#7i#W7Qwrgu7<4M9wt)wqG z6`gTC73{$B?<;$sR8k8tFok3{i~I)`8d_h&T`4zx%`mc{3cTz)(H|BKMGvUur)pG0 zS7^TPO(rHkhfwbrN&V;T;Oe>pIL$&&?Dhr5+?UkFe1>ADf}j72_wvo^#q<0~hz`gK z;mjM8jYBmt%9Smbw&Q%7l;~ufFGGE6R1FM~6u~j+)*2yTXHp8TR!;!2&FCh*eTzDw zd)sGm27&bqQs8q#0tQN3{&TD@;V>$b=cN5vWjA)nQdv2q5NXjIVJnn3$+ZLHLx^!P z9dqP2^X>jSB&+@o<|6K3GmDt-gujW#r>Izs>aO&ILr-SEWSnOVP76BQ=hq+$iA#b$ zT@5wi;O;&~Hft=IZyzcT?rCE-??8d9qmg<@x~O|bjN0=Y?1f@ugnat3Y3d7+V1z!? za4t57rQG^nglk!tL-M2|Ga^eU{Z0S@1cee6mI1XS0>K#Zd066Yd=oZGvY`zaRys=1JuGt1z51;T>I(#cE(S%R9}ifriavz7_0-3G#*^ zH}5nIdrG>?PcdQG$bkK=ovd`5KS57pPc;_V;&Udi?3IWmBBvv2!3ZaF1 zhearZLxXH;OeLIsz^Wr8GF(IcioE`$0)fC=k(@`K3E9}^d91cVX@zzePaApJb_(zl zp~olXWl<-^$KwivgqeXHYM`g>Yqb04>j0NcKAM60ZWe^~ySzfg`M!@m(?8;6a|ZKM zd$vj38sxL^vqu?C-y{3EyjP24jy+D!)(7@&7#-FY9228GL136|-0Un>Tv@;Wg+RDO z;_iOy{_gGdB-Tf-uda5XOfv9ByzvhYpNTenTL9YUOoRh2oy-DIRXq`oUG1>I;UBJ+ zt(V(N?Mt@+C20BO)+Gy)mtAm>NASDZrM_5BbP+(Lq`ul_UgdXFjEWkQ7`%r?L@=A z(amman}EpQb%N`S?TP-GPR(x?(G$Loj4o^x&o~f`FCbq5qeJe;VVwm2@e^Lk$M#Re!-ZabNvZ4)tbYf=NF& zq*EIwtYOe*16%NtOnP_~;NBS&X@FZj_L~ zdEE-SKCYHZXYNmwKOc zGPo^@3ds3WVSIzC2m$n50)Ow6&7=?LTEm$DUbD5Sg>KI!ozY3)BQ4&_J)IvLHdpTv zvp{kdLQ~u=;0N@?vJy=t~5(2UtRya^p&Ee;l zim#9!5quKSbXUDPtlruJSM8e+N;pu`UZ{+-p$2suzjDNI)s|Ktvd!uI`m+m62`{3b z25uEkhv6~Gq>;^&AIONIn`z3bS`cTDpbA+ldzE|+zXMDt(gkt-nnXVhi-mwu^h?^LV*n(!Y65LMFIh2->F)Rt0iz%WSnbj@IY2IDC~4ex^>dHRkQ)g zY;DTZ=sJEV1aVni&>6|mY|F*ST1Z%|W#z5Cq@~`IcSUgXttOM-tU(ltb^XD{^}8J~ z7r#D-!VgTI*BvChqVZX!l5#RxRhf?POT`e;;?0`Gg41>#NZ8Dn*AK zf(GPJ==n`te^Z31u`y*LB!|n(&c=v@+Tf$MaQy6KE=B$(On_Y0cr`c^C+P^HBC4xg zgt&CzgxXnj03P??LTw6-Nlk`Lt+8?kW*{!STxSTqRBdQS_c0Ybk9+QLdgus^Q=ld& zr_ANhZ_dpyp=pv!3_>hPgk%9l$OxeC>Px*eVAfoy!$%~(7cpQV z!~Z@*1kjlJT5Io!4sGGQDageXbrCKM-0<`C;*xF71?ls_E9{ovj|AEj8flwIs^x=O zxC~W*z-Djp*FV%R#+9uYBl4IH!d%I@;OI98R8O1G@z|bDC!TzyaXW`Qv;sXt*EPORZ*!qSu*uYD`=YeeZ5%Zw(IAYZ{& zb~X^c+5CXAlTmM=vz0qfqO|)crI@s7kTvVxOYr%ygFS73gT26nlq?$&yf@Ogho)Po zq6WN9*HF^VcDeG8x}!rcl_HFkgD7DVJ|Q4*Ad|0Z0?jFp4M}!wJaTd>($- zQZ!6_cy+HNK`WGNh#yNq-w7sxXAYftzMo4aZ2*$}jSQV>V+L~A=Qaoa{<+Yq2(8)x zkB*eLGENuo?(PzfVQwjR z`-iSxL`_f408fZF-B{(=$BU?>RYI+5M^Rf$x*VVE-En*{QAD;mXt-hTQwa=R0v@TQApN}qTm*MCw&5FZDPu2xreys~e z#yf9IZV^d&4rd45@9xk4;bxbpP|&tXa!jje5uJyIqz)agS%B<>6pghqzF%^wV7}1X z^*p|7+$gu%q^qZ(z_>a`LB7kjuR-+AMy#9_f6?-0d*?#XSLw!jO=->acnsuC16dbJ zA4?GTB3bM@9UyaJT(5dDd+w;JlsTaU zJi_nP<9EL=uj>Yr_uX~J-i%128CTmkC|gs1L$zO9|8tkxe1ox zL9Qa5dE}cZjv2A<61`PUd{+OCGx!T86n(#2}CI5m1BvW!o~@o=ENNub1*q5slY58Y}=QCY~T zT`T;$;poKZAgD3#XxLMPuh?$=vtqu|rsUH_5o#RwD88%d*K|SQ49Q#re#$LA@x`S2 z`Q-zg(07VAOX1k*9P%cZ&X|LK;u@$J^tr*OZpQ8x zqR2DTvvZC5rX@@o&;yNPahe)u2Q0W$KU90F-KUkPig2bmI-5h98RGaa%eAX%d3a@k zGdH&LKUmn0F*+jeATC^!*x%|zby9_f!du=~h_!@4yo7pMdJwIK3ON3WG$Y8Qtf3QP zIbogdS9UMC0|wTbd&{2h0VSRrl%7*%rSVi~ed>*EHaMgNXfepCBqfP>dcF{qdOS&N z`P$ESGjE}4KsgpRyz~w@IXjH*QNtn1B>ALIV(<|q-Hdq1J9Jm{bB03PS$|GDPU9vQ z>Ff_>?UX>UK`n^y@_3%`*Hkx)(6X%uqgkM zAHzJw9cxIkfV!~T^bX$;(=m_1zcI#3XO_+lBNVxB;8Lt(Ng~u53ch{lX@M;aBK#k zy#syq__8LA#VM8N>Bn&izI3}+v6TTOvnYR=)%_QcPKdaG!!LrNoB+Yb&^VLd3bLiJ zCF>qPzzQYOtO{ki$HvlXzW+}c)m{^H0bMj zrXq%JSFeYcyVXvJetv!)6OSnTd@#uQo##PlAHRLO$$h>6y|il^a`;arBB6qG1%TV4bN1%Y^V`s}f5&OW*-#Tg3B%P`$>>nhHa{K;32&ur+YjM*Yjc$sD) zTT+c1_AS1d4G1aAY$Y7#fcD9-O)6{tD3nMOJRF*pUni2*)NNq4Brr+(9IJdeE636= zlTqCiMDJt*q-!{b!Ghg3cTidL0{i&KoUhPG4#)B&{v3Z%clDh~%$GzYKCcW5^>on{ ziA{YN*U~{5g_T$Nraq<@4`q69Hv`}F{pmg+XI_azLn0CT@Tb?Wlcj~9vvq-)MIr{p z{`4IEX)Ja|j45^;g0;e3rdMp-4>7mxzp-C4GV~b_7Y|awoyQd3f zy7@p;HWUZi;mn`TcxTfT8uN)xR($^bW0b^SzF{-W_$&=Rs&^%w))WQ~wO~~>{0}g) zz&6-8=YYKtC3A=E7+RfQm4HMm)_K&Lrr6v1&b%%|M~+5u==%<92q1vK zE*Q|BwO#e)?JJ`VFNv3%L=kc_Inpqh+UI*(Y@Jbg2J znM!9&>0}WH_PT!C^nRsaYsx<&P_DT^9`c@l=hx1sHpRF1y-mLOd%|wQAY^vPnTK+3 z%<`;gj}7s7;jcOOIJg@mqFVN+c4L+2mZhLa3?=0}Z5(?;Hzixz;npzlmebVq^E4*I z>6gbtxWxh+;iQLhKKpX_dqJyap0l6+wixxB&yKaf#$_BFx9#_7U&4O zEM^oEbk$cE^E1>#PHY&YGY|UK7DE+EO=l7ci_sqshksVS3}A8+5Yl_YSa~&4wT@2c=)LSL9sI)aN-OfEP7aE z{!~sv2%9Irp>kD*{rh0+!Eo4U$5t|i7CJ1=T;RE7*>*y-LgSa|rbT+Bp0dFES2=vR zAEl}OsPy=8F6WIx}ZQ!(8m$&uM2~Vp$tkCF=oGYkRsI}B12Z_Gww4N*S)IQ zHZ7LbDz>JE{UxEe5&&RWS(sK`M|*7lh6+XW1G~bfkIFk-N_R%z?E*kqCh|stH6mbk z+j|qKoHgtl=`8RzPIXZTb-l4(hzS-e|Aw!LUDw)R()_B25&cK;@>mK%aRX^WBz&%* z>;Q6|MJ3zy06qCLR}eJ4HzPCRb{5l#%kgir&f-fD@kLRMk>c{);iW4m@VoXb)RiFl zM$Fh1g08cYeRPGywaoGqZ!YRvLJ4it_u=W|Un~#y6$w?WEUC8{btwVAhmcP({SK$+ zWUwNyOt8f}A|ftRGV9Ffa+2Oo@bVemyG{n6OmhQz=KpTUYkjrpZlF-}i;oaus zh=U&yR&AZ=(TpcWLScRk5FVpj4ihK>iZJR1 zW1*4`IX1KBU?w0+W^ZiG9Za8fL!TG3hXrEkB^0Xw-8XZPy9`_$V*jg6U#A5&_U8sp zdU%Eb24e+TnBb+79$3fKFW{b2%7z5YC-*N25ylw<9_13!6*fUI!>NM|9|87YJ&F33 zyLKT!MnBz0A;|)0Z78i<3}SNhP{>Xs|8>ei7{z>FN0* z5nc7G)&;n^zG=9&vRZ*>`rDcS;HbVCA-9CiJNp{~{h8hPwiayN$5TnVapCM85ODAQ zZbiwfD5({G41DwRdxku<89nMUrq9tT5s>1}Z;J_C|BV`5q`+s}d}`Gak-3;QOy zsaQ!`A{nhcA0am>+{1Zij770@dT_UTwL5Cy zJMZ{s%1=RWr~){DVqKe5IyGX-!6DO*W{^Lvr-B^`K!Yyh(3q0f+CD0R^|zlGD)g5E zJpqBguV{LEu(NDN7V-YT_0!s6M<}8q*NOo-G0aKW2QR3Hs9|AHK4O6&HD$EwedW2R z-{DP#1m4iqb&TAb7|9J`bhfNij_#tU%6RloO;y!O-67spxX+G^HQB95mVJ`oSVoe_ z??nASovD*ZOOwH$KOr|t&K7A_UaYGsdy(A;eXceP2t8ZsLJT31>7eV}<$v0)r00lf&j%jA^t-^XUNP9P8HR?Z*;&UK5 zJBU5eBmY#?-BZXQqbakHZ+7KFpuinGOtE|`kMM$o>Ekp zlR9#F$$+#1yzk9rVLP0-C~t;d)cih%d87Pq8TOPo;lKeq{bp<LpWOHlMju!O^{PEt^SMBx_l#2f?4UppQwHJm7^zA%4QwV&Bjr&!_E4~D`d2VrW@ zC@1|-01*f7_(y`_Vkpr6#CvrN90S4UyhJPMrN~6FD~jT)VH>`O_`sh;xJTa z6f40J<`WE|*Q^7))9V+Kxm4D**@EnJXCq0H*8_&&*JuJQk?>*IPI*N&MU!D{f5ck0 z5UdsupJ2E|vkb6Ir!`MBo=@ms9y=BI`$bc;!EFgrhxNbpZ$B4Yp{g2hXPk&zBIV1A zUocz-!RAN1N)>SmhD$7WSWUHBcDR=^LO7JBnZ;KxQMOXP zJ)>N|{R3Ap%){#<0gCip#Kl_OZ1~PydnFo%EZ9m(C4RxGNpMwW3p9YQG3RT9J<&YX z?EIrOJKd3_-%C=9hFrcrm8)jKqe=Qnh4-9fGQ0|g`H#U6VGs-xG|w?f0>R?}mR6Vm zE5?V$g+d7dB+X$Vv-BtSW|G@v(5uIm;r4XFTgOekTAgW-ZiyqqZ-*Ivhi#mL37Sc; zjBE(jEGDeN*dvUfh9FXdWiVg0&pac$7T z8x8tW?^f(AvosLQo8W@NF0vE|Ff?F>lD4cat5xzFP@=8Dajy_r>?207n+(T2Xk!4u z-C9qV)aF|}!UpVf5mCcHy;Ylik_N#lcMo1sS;49~z>-;FStSY9B;7B^l0n%Xhd3ZjxW1;pc0`8W8kz-}DbXrCE1W_}8xsrxwyeS{R4@%N zPl=$8mlj#8vqES??SkMYrZ2?s$G10EZ?-?x_?mF%IU>BTiwvFjm0u9d+l28ob``Ce z0$jyof)#}bh5+*r4QCb=<7_aEHWUzC|7-+Or}6UT8!|j`HQ_z$x9CS`i7fRdf0@QI z9E9P_!&9RrHo+PM7zQj$!BD(;@FbFp7^_{ejn)gtB*PPK{-~(W(}e3yY-D*|G_k(M zzpjJ;gJ9lG2P9b0U<(a?O(mKbh62rv#~ax)ZDUxr~DXo+Vr4>rK^aE()iKe#94Oa*)hW@%~Q2Nhz_Rc58Z5WKBLNbxD zB^iT_|AC2uG5CPGfnZKCH|Yfm*>p9VSW0thI{@pM$aE^ z)|t(ds%KkN``&YvM9ol_DB3u&>6qK;_}0z7%T20%??lJx@c&KPj&+?J#4@$+P{R#O zbI99eGLKX3R6nk38w$`GLp7;j50Fi|;_&X+xnwxDMmPVu1f-TaZSqKFqt*vIh`%sv zw`&L-CJ>i}v~0N%-ByeN+SDyoCRN*m)ZK%h!e0YI-+ZhIgKrs~V!|~Tw|91KZ3sTK z9-L5E>DZ8Fo!SKfMXA>cr%t$|4ujpfMg0VHGSEK}dAUAjCrg+hW0EVSz^o*aYYn%4ES32^JtEh~O)D zOoV2_B0K^jjRXRMl$Z^$0{UOLk5jp^+vz+y6HPR}ZI|o#Uc1W2XRfR4OlPjOQ?Zul zW^0K)tJV_TY%S4e)moxY$vWNE%z`+Xip?xd<2XA8(Rueu8k)GeES`d9=VF)AFKZQj z2)q5bJg01NOkAf~1JPHZsHU-S`hQfxF?o(1FRW^+w2Hn`nod(N&F026-MJ!3G(X!J zeBtOkN3rJTIMzy>oKZ!G-BYkBl85SQ~J12BH+Hg|iGMj?v{3^@W$5C(I1kNoU zCU-n_c{KXx^#-DeS<&(3AE0QV{aRG?U7t+$&#u>YG%5Fyk=SSBk3wkq(Uq02^;sy1 zBsnh8c^ywN77kB$bTizU{4{vww}ZzW_l{t%jt`OK)`U=oJTbAn+ z1Jk!3r0*)rdJ<2@1^w@)*pF37M^48YOG``NudIB$ zy1MF^W~M$l4AJ@Y6EEP&mY>3#zvZ2^t>O03Qp>`4hTa&6U8rX1NZ)bLgK%Z$-FM%m z&DWx7T9a!{(Jz0yyfwSg?8BWHZXNJ6d~P)QZ<<(wA)A|J~nlcC32ed=R9Sc{{SMWpH>$BzN=B2^nzT>-lh3M~of9LJ_+E5y=>)v3VHBW!!(rZ!C#Bliu z6HR81^l`0K}01S@hB|N!gAY~S^J0&4Zl=w4028#7l=k{GBmxHJ3n;8%fpg2Z;suh zA*Im!Pc+j4%KoMiahju%}tB|AN z12$y$(^#?iCJctbE<8Z9Wmt5Vx4T7Nu6vL5FP9;7!Bwg)TKi0N673muh>)NCm81=# zK{bz>X?=NfW`%zw>yIx5O7&4wM`$!j%`QisHI3)EY=fW?q#=?Z0tAOrpfL`EL+MZq zuphQ=35K*^E?Ku*gvxdJ!@akC*-sjWB?slc{~~27qy*9b%zw&)=J8$R{rFbs+#O!i zotc~QN$apgL&fp~m5jjwRL!2kgo}88Fm#=JKhD-19~y>L?ipyUBoM)17nt?{OY5^l zG%LDGibnet1)Up;(!<$uXOU0lBftt|Tv4IcinnVgZMId#l` zI`0+j-DwJ{)3-40pgjFG!PFg^=zNzNt3YBPi81y7TURU3Nb?&uKit@u_G|6s7Vmc= zW7zR1P(kX**S4ruu3P(jsQ88AMH04V5IetS;0VvW`}AQr|GB=z`^xSDJ{S_)ZxPK< z^w)%sFuX&S@XUpK>KH%dQkOmp=6DL!Tqg?$nI9A=-p#T~m-vARsox%4mN#%dYhR-6 zcb-%Uj4=gXff=YKw7lEG#)_q6CZoBuRAc55H$fd?HMoU!X*X);`+9Uc)%1 zfNn+61;>}`*8bl9tnGvsHp1byo(ugCkc4%U=oI{(vRn|N6Op-yI(1?fHfLDPO!dt) zymLoM9l6(_5XQJrS>l>$WiuRBaF?+hS@jFSd@hg8-xXXSa})w;i}vmO%05MEqN<%g zgkLZboh#?0XvT$aAN?=rmD0;)`R&?{VqEX-%w$4Dg6S`r=;e2Q-3Vb(+q;~7|liY>m53p>8iZ|5sG zUGNczr=7?)UXvPcI7jA8gH1kjKlrT2S*L(jqMlK-b-a){pZSy)}Hzc4u84a z;DatuX0i64PlVk$RkX<4{@j2F&fuq7+H)@Gy}{Fzo}0BCEfd3u4@0O~$5qs-_D3oO zu7Z9jPY%$diKCn!h>T z+6T1u#SEhymarYd4d+|xYbVaF?(r`qFlKr#%edzMBrH=*bVt0ohrcAc-qNK@uespd zrAwidUO;~jG204jz-$RpmJOw zEDu`IcLIi`FdttamwnPyB_8LH0+<0}foWxgI{g6a?JKtK+3|gGSbMCW{s%B8Bg6d^jklAQ zn)^`SKv_Bu5%q~J?iX|6*%^o*_!{;R{&?IKHo?z>!FUgsaY~h#wv4fjQ@Dv=SP%%^t{Y2&$0)ifN8FoWeP$^V=u-)rhul8+*qZ)V%5u!k~T< zqJlPt`NxG}^Y;-?n_?i>jd=Lw^tE{THJ%UcPsVn<@JMEu3t;#w#|Ok5nM++pi=(Px zwO9}l?(vhMvBws~7_~&K=ly1e`i`FzjqEJ{&go7olVnttf&~Ulb@-bwUj>er!u!R; z7*W7fPCg$~hxVH>;;9C#^aD%1U+W+3(b^(Dw{pQ-7gw+o9DAH9?iKQy*#8+u;KzV$ zO*6y1z!MP@TKOjht*Z5*9__Jp7`>INf(h>95`eAjo1c+DJ)d#zF(BeSIo5s++xfM_ z*ipkiUhm27`4@#iTD4HbTi{q=3QVi|i8{^#`Xw(&G5n?_kwIRk{;J>e?B~#$%0v(c z8LsAxTi+dIar17xtYT#cEOH$a%BS+inW4VsSDtl{Fu0a1cgMDFf%g=|bDDx_`ekD| zI<(KwoH=C7dHKziYt8U?T~lEyZ`p`7QOjT9bmkgNvx-cOF5LK2r?q3*jX!N*+8(i@ z$2pcez;23qde~|0*!K4485_&CNzwX+Oeglst)8c_TQ*Wo9sDt@u4tVx@|OeQ*|mb6 zPSTjR%t6uP$pf!jpX#(WmA#(IhsQBRM^fVBZkDCbw3%bN-0_W$@REj62Xo9ci#_&c zr?n~U#$U2D(+}yL=D}UYOvmntPHR)y^$y<*<~oD@ZE5L^PHR)y2dk?fv$G$`Y=bX4 ztxZX^@JMWT{39^oRdX{ePTj+u)~2%e_3cuWWsMV-IUzcY-2hM~PFT@t&=Nf@(LGJlS#4$*MWX0L=7nwQnBLwaMRV?G9^NGw+__#$ z^ptiT6m6-)G>wTvORjux8dB7GL7ZR$9Z z-FR8IKm4?p=pbOGldNfYC&TEJh;9}AKYM4-`ZN@U@hjp>Uj{=^e48X6z{_bX)=s7$ zz#;{4QZk6*P!L^oad0mP3gVy?6kCT#2cdNkiWY4@l;=6#ay2&%M%``>{L}U(e}^BB zPo8s<^k1eL(^Ot&X~sI@ID0aTj7gt^p!-B&P=PK1dNvb53vX*3Oxm#ys2M+f6j7z< zaA;&9dV`TvfeSv2pXURw&*x7N6Vxm5=93U=yH-?q!~doXiXHm z)?vo~=nGqwNB@OOGiwJl7Zjbr+&7Aj<4Vzg;nLHkQd#RDCw0iTVqqicu2S@0xbz~; zf(~)7wByl{y^a-ApexX2yi+6!lw8J{$4ji0=YpSX;f|m9$d6PL8&U zwt5C?;T-=L>v(Zjd0h+|siT#(4*Mi06s>a<{e3E>{;(#bye?Qr=qTDeGkXr4W71+B zE;%W?@N%mPDj~lgH+nqCOC9Fxko)FJR$yB2V$k!BDyUUQd0m`3G^dW(g0@ds;E61A zZFZOgy0b<7DQGoTUKgyxxxxa>M$^)Q=gvANT{W)#x-j=#v&Hpz2+(~|bhi4!S*CZ# zb3scrAK~Vl?fB88y+%TRIK#HK9v@kL`{~_FxbyQh-fv{5T90cB`K+2tPaerdXBZkq zJFQ`4ZrTm>;N)J=D@v8RM=Ac93rc+<(!6k(mi+k@@4rt(sqyF!Bx?=2#XE2F!ZTU$ zp`IKMF!k?LRvgAW#p##vM&($9D+D*LS{|NXc?S*^8 zqjMA;*@xe7fu#tv3Oh{hqS{RKGG%F>SX`ygV0wK)G*SoX%qHOlIQX1S^UfQ9e|mY9 zQa>=wzym*j4tA-!XFtVa^=Fyjbl7@G*Vzl7HeGN+&=q@H;4A3>|@g}^)ppWq$ zW;tiLYC8hx224g>HE)`H#686DM_zO?m`8_%uh(>kN+xy?1^YY0F3`2^+1vCa)SooI%6MeTjES9j>RL$P%j((e7y!db3@7o`r!xNSkBjb!lk9ca}^B+^;XR?&O} zNPD4aGx%qsu16b1!#V~9ppULdzcQd<%_ysAuJ0kd8rJbDV;x+Z#FNx9478a?yD{2K z@3byfbl9byJsCQnf&YBwWN1N41o5Cu>VTr%8~%4sRup@5ovtKpAt><<5`J&314;Xc zfM%p;k^Q ztV1xvqq!*7e|iJW^D##G+i~WJ-Sw{*a7US)TjfXnVDF4^8j67++R}bmRB3a8FB?(N z*?|TMP(VVWLLfxJ0a8K(OGwKB5FH&2;shw%iezmo3j0%K(v6E8U^Uu5JAUhwk72Tt zVZAdsa?}P2PH=r}q#Jf0qFoZ5=sO;>QqiWIPoahn^*6+s$Y--B!;3FNDygFD`r8=`z24CM^`@DHT)PN3f=bP>QKZYVdc(H|2B?=hA4V? zD_`a+YcUz}V+bqs_t^5$+M+W~${vj<^xSn!A8Vm2rxfc0|GEl(!qx#&^yGBa`kQCg zfm8JLvD}-Ow4_Rbc{Hfdex~Sykyuix2LI0SV+hNm_ixOKa(|$FwAv0%hJZq^+&3*J!_Dx~73+u@pmEz&^v3vT{n6k*(RF}s!M*Q#KYF0cLh7UG zIv{*>WBKT)%_A$C01a@@Fj&_S7g>iV*MaiU>N-rBGav0a8N%4R>N+f{M6LsA@0{hM zD~7H;nl~(<$znatwBuD) zMv;yql}N@!0_)nfB94zn8M}MuSX3gg|Eu`uyQ>UCuM2%*m;gg!w6$M~!BV{&Lmu z++NuxyB(}4&?~f$LJCOoS|p%I2B>+8s|$F|@tLrM#z$wuJL032KRU|mD6)^B8lB_017k!sT~1V>YJe6SjiN=jjYdh4g8Rn=XnHy8TQhk%UQQZPXoDdaBo={=NrKz6$JV*$h`{%Er>w+11u`=(IzZUL4M26vgrr}=my+N zEWKK^AX^qc`q2ukAnsTC#|7vs+P0r+_t|5i+qlA=RJ zlaCd;Y_trZ&F-R%ueyx8NBQxSLsI({V+JMk5lq)=@EQ9cdq~ML+uV zB_C!zQ+S&MEnjsP6GmsN-9UQ{S_9$YIxcc&5$JRo;6sm!!f{ztLK}@ZyW*pDG@OF~ zqTd-NcfT{AcBD+m0WF@@qH5Wst=Q9TE_Tf`&DF<5obk~raeOq%7~`X7_QywO4B2ib z=Ia=*MlU*J?@AJg9>+(I%|^R7jwGn#_-OCY^w{E~ z9s3#LXsFvL$LVk5ijVFOH=w9p!Fe$@9fuYlZ8`qA!OaQvqMG%zybyO=+E?Cu@7w%HSnUR;XT zqYrCQ$?gxr8XfaTA6|TPNppo_{^;?>N4wp-68atg4u5pQJO1L1k4A;I@zHMn==3g2 z?Nr_ySsDwGr#tg~uid=^B@NY)x^#J0rFHq(;-eSc@zHFf-SN@qfTcY#1kmZW`*^uH z9MIE1&l(8m#LhT?J~OWP=()Mj_-OSyLV+GeqdOshZmtXmbPDKM0|7mm`@aA!i%N)S z6o0haMpvjaBLb~*Xh2W>Z-BnT5pJM-=n@~j1VIo1G@6ceF`Qx(6SgAq)IOm3a(l_% zg^8M8FEx9=0G%|a`+(Lt)&pE$l;VC#n|cssL@joNeSSy=NG&W68Zt z>W+_AIjtJ~DO`!&I}gD{tiB6D8(tE zQJ+hG_5#AU@BkF_G3Nej3FxQG!5ZoIyL5dTJbo6+boL3pG+l=KyuF6;*tP}-1Jw0q zzTBq>KkY-Z0khN!x0h|=W1seeIzQ`v1v=%!y$JL|g`d6&L0!83F!tDjk*>+m6A z7G{)a{%FK&6}fv^K!pGAm)Q{c{cX$c?g41O+85eziUWZSZhRXms3v|NQ?np12(g!&yR_PSCm!?kTJ&9pu zptXOkEZxS}=cwykyv{x2j^_t69d3=bFJ~8{Z8TiFUMb)gUV8?U4LkYT*97MMZ~H}Y zDDuxXC_KGB8F00zin|h(B58C7KK~w;u~9^qu@H0XqLwBkLvnYu~99Ov6*y8J~epzL9-PZoWzk_lIf!jOjxox6=x0jAtGX z;MrVz6KY96ztaE)ItBN=UCHCmcj0ZoK4!oB%yzw!!+YjYhd#dq%|U>s&^vHNlWKsp z@<)dQUG>i8y>mHKrvm9_Vxu}qq0M>jDaSmc?U-*B#jcjG@e-(ez(pE6JbjVwYOrzf zI`o*LK88~+RyhB#mG|&ZPj?Tj)2wM&v_1SOwc)clJZ}d>y;=Rjr!Qz9k`y>B7=2mC zPwg}xXr};c0`#e%ihp=B`}*I1*wuCH85Z;0@$%(^*k}g{EbriC7M0|kVYrRX@we&B zleyOKGf}`}S^j1Lf<+0+S*Qy*^vx>E9##F71Q|Pza4oO!xZ1%x&lTt~BCil!jd|30 zYDG`?0$oE?syOChVH$2xGJ+ip^=1^p&@+AD;nJ_0Nf1TJH>sEgtooOmFOt~xjFNcN zXYG0lYSc6CcOEx~*ZVwU( zbOlfXj?}*kZ5m0H4GYN%WyMOrQ$R9rfPNEiY9lZSs!s)qq&~)@L*hsKD+pZ#&}V_9 zsSQ)dtb1o}Ci#{U4zv$bTri+-&?f`*shs-J%<>HK185(nfi7 zv=j>T6k?Qqq$Uls*Kt;X4m@+SM?J(71?>aDOD*98*f%mjW1p?pQ6pzYfo2cLblvj{ zoS}-YG`b_>VhIyPFeDO9G7HEE!1v-reXfyDnVd_lESD@hwy;*9_BqAey zpl3fJ?j2m0XzT%veSWKD{OJ4Bjl2Q6Tuiq1y3W%YpIrNPid234{LyZW_R5+~7(k9E zR5C;0?&Q(CV_i4}z#~df-BSzj;T|#_DV`ZBlKdD}K{Nl-$(;q7pJjpO@waFiPWn7E z6sXAM<(pY`vAq@T1J62hW&)Vq1KR8K&J6ESpt%j-XfzGjxz4;2?2N!4UCMOvM>CUS zG|=aLnB}F30Q)grhP)WS^aGG>l2IVzkS3q+ea;}%-kt}@vk^Uwqu)cc)V@MMC zB!c&nAFP?v^59Aj(63Y=*OrD6t;Yc_LRF!vFCVkqorW77z0o|U%< zB0-=hBeCbOeT+?Ad_Ga#Hx%QudSth|;diP%P1>6iZEZ|0tMzq*YdemQ@)fOYRcWTB zuQLWQ9nK&}MVeGPP8M>Df0g$(tng1hh`iwPI&>_w1cPyWbiXv<3V`;A{Lvb!6vszL zNQ%bY(fOloe002-LWrcl{9<(e=u!`VbTAVKIXdL@$o$b6Rr#f(b~ZcRYyxc)FzLvuzNS>B!P{N zkEYOO14#uU+ZZxC%CBpfmaEAlE4(S0i$7XvBo@O0Xr?U?r=m#9HUIJ)c!S7p)d2kp zEW4U~_NZb_>(S0Bc!~a&G_}H|(N|Q>$4)NVE=&Xd&3c0f)cKF(fu^HLiljK263!pZ zM2-jZM@v}kK|l{GP-l$}m7@#v3lbl_+xzm>t|L_``I80OUSsd?J)s(% zq^dIuQQKPgj-0%Ri%R{e6&V}x@8*xz5Y9J!XmW(0m#V-cl=b&5)Ejq|p?+Rs@O-$B z10Yr~K3T8P%s`gUAXi85?yA6bT<26a|D$s7Fn(sHf3-Mx%fr-U3nYee_Q;v)(D-Op z>7|S_K3aj6ygULr(1g6{z(=5G@MsyJz`#-R4Zt!1ZLa~7=_9z!n1D=Rpd|%Se!^fy z8pBVN3`;+%<_8`zaj)KKuOatcD~HvK zZ(~~Dd3dXaK-+7cY6N23vtL|-I^ucZy!aSz!G|@^55A+YWoSwkqQo_0Kyw|Nq_!kK z@Tlz;?xy-F@e9N2ZG5y7XwA#Rs8$J$2l4qDnC!!Q=yBA4tH?GQf!1p@Gmyl;#`DT& z=mxKH#@gt4cA}xFS%@0#{w3Dg>oEC&M@z!UtHgMi_-HrKnwLjkexkth34#=>44CFY zohvZVlR0yZW(JykzfO^PQCjmB=oQICY-$#wCRTc$0&VgGkCq6~cA3zD}znl)OIvi!ONZ8SPr1^@%JTmy9rbP8%@74`!S258Mf)D(wV1GLEx zJX%8W(XQ=5?5nIfj*lKlqwD)LdJ^v4?(Ea(`hJbhhibHDA+mh$LtTNE{J^8;-$n38 zBi4d*5JZ~}YomKLnseeT&|TN0yKQu+Mr#(LX>VD9eg%%2#~~VBKA1l`+D5C_L9Rrv zMkB~rBcQ7^80s|&wDCGBXL^8M>C$M)LS%iuKo>I5;*xkD;B>}2Nz zSff+0magP2&^~MQM9_X33i#_=Om{bCo;l7%Qqh2%?enp>`F@&n69 z%1pcBqZQ&V#M$7Jmn9SBaGqgb4nV|)`0!3>&Gz(FQWP&!`br!OkADF~T zkC0G&^uhemVO~c~O$TDe%M!gdT6QNymE0tt^%@H_=lccq9-z-dku>@=11(v8ESmri z@m(oDYw`mtN6N!<$48q@luN=AHc)ECcdmn)WS9)QCe7znl+VkVg=iB8YgDW8LywYh zE;yQp-U&;F_Z5oyqgf8|Ex3wM{Vc}((c_Bia4@2?PV2&d%pW~A{^yEyZTAJY(#0PgXJw6_Kic9fd>wIow0F4i(I{Q<(O1M8J@)u$Yo+7(Xe)=uAFc7x znb?nJIVApQ8z0SCS#f-Hw2aIjef?4#@qgGMVI>LNWz%so{pcqLl)-_H4b+vqTMgA$6yN1sGL`pKv8zjgeFU~(B!AI3&gd~|s1-5mHQaWEttS=O}- zsSmkVmV*)9=CGW|4cMQwqFU^(k2V;V_)nurPiFcXDlFLsR$%$$u8$BOtxz+k(#c(q zCY_Z(F(OpD>O*E_ExHH>Gr}KzGBsLZ6;XU$^%3HuMM}ya4T~p}G817`mDYm9Q6Fyg zE;Rk^5?KFR#ij%CxP77^aReFe-p?DomKl&u| zM_aSejjOG%t4a^!-l5kK^G7=;EUL=Zc4++{4u-yiuY+;U$i|T;l0TYI6oloIMZ9;4 zI284KG^f(ZbNR5%P>ri{XmywH5#F&NF_Ll{Lu=r!P9`ORSW$vG!h}ZRw&g^^c$AsbASEy*HO%5 zJxHw6tmfE&%&FFX}F{U=E1CX8CIf-cQc>(%@)$%Z}Ben5mGn7Ai z&b?iA#%{pwR_H&rSpNF!SJ(OUE=#R(jd!r5xADs8OBxFiZ7hU6_p^)gLFqQ%X;0<` zAUl<1B)jJSi~#+sOvM#{tT^}-KiX)9@JG+h^$&(6UhA;Yq;3oS*(%GUZYzBbSlZ=5 z(&Hz9$=9L;pEGN-D|z{NmP>zmde)MiMtU7R zQG=X7*4~9B3bgoAuA=@4a?A;+l;k$kS!R%jRJKQ@h4sVd2bcvM*0*9g7Ow5PnwR5SWmof`p^ZCxEu6Px-;4gzLMoPov2SFC%kD1=(sIAR5z3 zop8ZRnWdDTBRrvEDe#+L=M?-EZJ!|(EAqntwvM0hg~%t zoj+OwXf*xp>JaDd=UaDoKe;*F!81UYiL6w%!)-7V=|mR5q0x1(lZBj+)zE#D&_w7> z9r>36zB>f~S9Al-=PW5|3m9)U6(3d#T^Du=2(%?TJs>2^u8XQhcQ4%y9pvbWkCv%n ze0i&9rDcWZR=hwY*G zqm>zDPS>NurYrB=_6=H&gEe~In?}q|Fs6c?4#*#^I2-=xD2@I-bJKe=82%PzIt1t{YjlCAMfWuNooAnYmcFhW zKpa$~88d$?mxM|B1>UXzace5jc+O=yj7Ccv+rpTK*Ms3AY%R1F7-%IsjrKa!cF3m< zmEQR1^}%O`8XxTnH&Fg)b?@AZ{=Zz0K(l*S9Z;js71W#SpBWN(47A#4ukt~`E4PXQ zQr>E`SGficI829VG`I1*y3zC`JgQ{Yl@$eA$xaVm_!pcl(7K-VkK&_wnbZ|cs`1f& z#Dt}E>n9nRutaLKwb7@tG+sD>E)be*I+Vr~tNw!5Ky_4>QmfIeY&8Yp8m(=3IuUFc zK&Jv|GSHUnH0{y9M7Y9GW)cn_18vDp@n@{yuQ~5B z9cmp|{`~XL-#^;@^G~jbN)Hzj=8C=3mxhu2(YmPg-rBP}mu})krNL~p_!(C@fCd0R zXfx0{6GUISj>nsxvXok&J!H+i6spnMhI%ytKzVNxo@1be?3>6=iQyCqo?mbVIntkk zit_!(y{}#9f?)&Eoqlu_f3%K}K_a_3iJPRHp!8_g7@`I;N(>;;5z!4n!&Dxi&+ z)QGbb@}4yvG~%?a*U@LAwG9+f=U{ExE=dc}mh7~$%(q_Sj#M&$iYI;1;H-g4!_5rc zyH;d4q0{axboFox|IxA|#V}N-iQf7TO|mruwUKnax3OfWN&V+E9cmv`zQ1VfwtZzg z=*&>#qZdQ=%97pRe2R~be3gY7?fq+_E=am^sI;!%K{=(i1L{;t4^9(b}$ENYo(Ls*C9>0Jv&odfpW&NEURq*Z< z&talT$wtTg(SNgd?W$4)L9`9j_7NzHikOKX;Ltws5v_iJA|`5}7>L<|;5(>LG0|vU zHBbzMy9Ual;>AD^6dxaDo$BhX-L+e1@PXLY-RYU`-I)uAqNcj%%&BbFp%WH`*U0~x z`*qh;%00?T`q6dP1s5D6#IotZl`Cn@f7o3mELVpGZ@)q6r&z|+3;$SD%JHMsqwCcs70$m!>WHu$52b;FyR5b6NAEy$xMKZiAgVD! zy*TC@5n%K&-YIm1P@o&^=vS;CJxd!!tDHN;42%f&WhKW>{Ak9yX8maRvOz7d$_H3u zK-ytfCC9}&EPeb%>G@r?dbCbf04#Sj@rd$2BOb#z5BxMgnvt$sKbj4orn{i{^avXe z9${p3iFe%=m2&)O;Ebo>m1-*KQbtPWxM6ss69eWE00+{}J4g6X3!rl(M??Jk}nlXl@K7Pw!dk?^=lYX?-2bt0v<_u}~_N5L+<(ji0$xos73^Wz_(YdU{^rI`C zumEOp=i!Jw+5^pKetvpFEknoAqj`R`X_FobbxLOH&2TvNXaSo9=Q)_Y9LP6bfTmm? zo#%Dv#4tUaty4cb-VEW<477J<3ads8An&KJQ_-W9dspN~*IG;=Ju>9*F+%8Y@*(|G zqg>YEef9}Uo*ymTyTkD@xZDg!u%XV2aQKh+*{qMy;hkTI7S`3-Vaz&ij?CXpYv$pN7q@Q(~KNK z2f$6CW6h;CUwCAA`@skP_L7tSkJ7sW_pTq2u6I3p4(XW~O$_w9-IK6q;3g+~5Eu!m zpF#(bZ~4-_iS6!SpzC}JK5Ou3n(^p$f$g+tC^Q2XQXjto&|Ub^!kB1q!FW4{K^4s0XBsW^JsDuLq~glbYGBW!_2Vyn<27}xOjJV3v{$@Gq4^&?{^SY z35%h_5w+z<18b4`sh9D;kafg6-K2SQ{p@C!JUTr6)SRArw5>;X;79kxq5F}0*ZUu% z2VRcWLeA#=6zB$m*XGe)r{GkI(b+t@ukuy9Dy<8?5TntG$Zt4t+pXKTL7G zx!zmuyOkC?e5oi|ksq!4(V<3u^o&V*r}p5ZdF$i|t5lomI6S&Q+ie!tqj$tSTJ@v< zuin9(TIN2n_z`NB?-? z-wViS3}s4V7}Bf*-)CcVQ{+d(qeGo<&MqwcTXaw>b>Y!1tG~hi^P|nYPQeL4l0|AV zGL5~(;-g2me3Z3gh9AAFQhszMkEYZZ!YN94-PEI7>qqyoX?j;FKYC$NNg0i#ep{lV zYaUG<_|g5T9NVEP`O#)1SqDr();ppiya@S_+UrMSz7nC<^lnql>(TJ&OYQA4k?*1( z4W3dPe)PH}R^t4}&HEc2jK23Sc$Oc%y{sQ?c6B^{;{L4tY28Tg2mrQ)+q;rSX9+YQ z{Lk{EUBNyrFO`A*^44~3?D`kBAN_c3^JkSKeQenr5%H^>o*QY&`WQ;@xfGU3v;1gX zjW&LC9_S$hJsv+rVZ5w`eeqG-PS?(k1-6}2h3!IxLS|9?X(K`c4RbiaE}8bx=z zt-zkoI#!RDmX^ok@zQvlce2o+UC|!s(`XIozsD$kyc1-(!G57H0Y>w`4*JoOcc@1< z2pw6G^<_qx+d}3M0>&o-^lIL0H?R(RC7`X;=5X$W6s)Y~=Wn&-(Tb`fD_!fu)Suhy zMqIvWvnI#h4wmWq^ixzCIa2C*uq2p$u)@{j>ol} zCw5X4Ziz2~Q+U(Mq3{^4|65JtMtZZ}e4INLHOP0;4X>u4|F%*-1 z3^aZgo}on8I#4o&9e8#FkVIWV9<=oCv|H0VwqI_BR?bt3l3n09&ExSD7arnrx58jm z_(O{Tl!Eo#YP5afY}b_^t&2)c3Fz_oD|_ej+C&h=@hzpv#^zvI5Q;s?{sYTwB5iNk z`~ii7|g9O%5qSLEFPhPyYC$eBYVb9fNNob~iS3_hH3G z6Xes!ciy~tJKq5j9G%)gC%qlg1AzX~e+J7vGEgf&q*M` zcNJ)3=LxtgqBX|r01utfAGa!k7xUjNs^i(I*#iBd=4^*Ndp_Ftv!}A4(KDrEG`4`A zTs>@xgh0PT@sfgW#Byo$6zBtim}rg0{~adP3be8F1n5Rjhz-XE>!qr3LFQYWAG8;#e0 z2hF!heRz^R(4f~Wi(!)p%Y3#rl@g>aKo5mzWuURci?^kF<7psQJYsFScMWd`e)n}D zc8r}TKre|vv}YWwj^T$#0+`4EQ=D(8kUaRR>{Ly^gm%ac@5b?R*tt*IDpD(B%LN~4VdyJZ(X`siFQY9fG*%EY=x#|3Z;A)aYBWfz zi|=5jP{#YCa|VBZGzS`-J3iId(CDDb=$v7nkH+t1z62Igc-0Yx5$jrk$Fjn(oW_xP zgTHMz!_eakGG)}=o{wNEl;HWx+N;-0bUh~+h1Jzx6IE$*Il8z#}chKk# z^W-e9o{viZR90ab&CW;rh>jBJDx-@k@4(py=;@R;_eTd6=wdQD;_vv^GhrpaysF5m zqsu=ztM1IKGP)4y;4<1*8$)zS73m6eG13u6Mx)J9mU*Dls}{pzK(o$5h|I6L0-YZ; zdP>IWe%cF_q8nDyQ4DA;(bI>ss`H;6u~gELAGF75v~DV8T_rE5K<5UHjOK ze#3eXi<1*9NefGl1~f&0XCpX4J3Bd{6&2+7&wCwXn`UN6zBxb1bUn{p!Xl4Jn964u%f#ETdZZcTvB?DsU!b$=mshsH)tOlg+_~{hKKsd{SN%4r%ni+mC!%a zjNW7Vh>v88jCQzxD2)a)Ys_p9=s1i#Fz0Q;!PkXdXM%G<_F)-bf3$b}uRm-rfp&T7 zc{$$jkA#-aF++Eqr>l3`#`21VrBHf9N2A=$8V-1PfSpuf=Wz-W*~NMANqp2dSm$O< z#o~%@;{J%j!H)=i#CLZGD{-*u} z8F9&Zj0nMPIet}8K6B#WB}ZWYnyhtjL0aFa@anU)1EoMPtk7VY=QG(WPTcSp7JP!a zp}lvrb^Otv39`(!hR)w{yO6tPcgJ-vIFER5hc2bLvy<@pC^bZ?T`A`Z4WS@h#H-5W zU&V8M#lzrb_ybe_vgX8`=l<{Kydq9vIz&5!x&7Xed&g z2+Df%9)qvd`*#1IKjWXvKi;1|Dq-lnVu$X&8NQ@6|Nah} z+NOP3o7~qO$GC1}FL;1E{(a0bM#aFI|J(WTJYkMg{;S|q{o^)sxt__T-9l&oKPV}5 zz5j3g-lm_4)C~4F??V+1Z`|mQl+VV_SA2m%$=mC{xE;@FBu%V*r~Ey>PV-6sOSrAK zum46L9%k!Vv7qg?bGLQ8Xdr&?CCx=%T;_2gcznt3LDvXO3vF<0Jiy5t)9^m5;ybbY z!zuHDGv6V9#RH66^$$F%KO}fG^&aY{#k6nO6PZ!9-=pD7>Aj>)>a-CrgjR=eTEPQ{ zBYz*UdJ~VtWj<;Xr^h+|eV$*mO}!nKu@9_R&#dTQ-MtQUj{iU}r5B%^HTzkdZ4`9$ z?ET+O^HcH8_0RoAW$N1Q_1%irui%%Y5eC?;@cst`EV|OF}p4Pp_a~e3nriHFYzNkB=7Rr z1zcv7Moq2gUTU;vQDZ*RgAm)`ni$oC`3l+`gy{f1j;Y?T{DJU^v5cpH1*4|$N*beP z=amoIc{wP*@+Hsw9yofzoCV$iqi|VXqe=nwYtw+n>Ln$J>%T7L|8UMgd{9a@F%J}HE!#NH`Y zw%TH { return new Promise(resolve => chrome.runtime.setUninstallURL(url, resolve)) +} + +/** + * Get the url of this extension + * + * @param path The path relative to the root directory of this extension + */ +export function getUrl(path: string): string { + return chrome.runtime.getURL(path) } \ No newline at end of file diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts index 7574efded..63a3861a1 100644 --- a/src/api/chrome/tab.ts +++ b/src/api/chrome/tab.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + import { handleError } from "./common" export function getTab(id: number): Promise { @@ -7,6 +14,13 @@ export function getTab(id: number): Promise { })) } +export function getCurrentTab(): Promise { + return new Promise(resolve => chrome.tabs.getCurrent(tab => { + handleError("getCurrentTab") + 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 => { @@ -15,6 +29,27 @@ export function createTab(param: chrome.tabs.CreateProperties | string): Promise })) } +/** + * Create one tab after current tab. + * + * Must not be invocked in background.js + */ +export async function createTabAfterCurrent(url: string): Promise { + const tab = await getCurrentTab() + console.log(tab) + if (!tab) { + // Current tab not found + return createTab(url) + } else { + const { windowId, index: currentIndex } = tab + return createTab({ + url, + windowId, + index: (currentIndex ?? -1) + 1 + }) + } +} + export function listTabs(query?: chrome.tabs.QueryInfo): Promise { query = query || {} return new Promise(resolve => chrome.tabs.query(query, tabs => { diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index 9522e3c9e..79b3aabbe 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -12,6 +12,7 @@ import { ElButton, ElFormItem, ElInput, ElOption, ElSelect } from "element-plus" import { checkPermission, requestPermission } from "@api/chrome/permissions" import { IS_FIREFOX } from "@util/constant/environment" import { parseUrl } from "./common" +import { AUTHOR_EMAIL } from "@src/package" const ALL_PROTOCOLS: Protocol[] = ['http://', 'https://', '*://'] @@ -71,7 +72,7 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler } if (!granted) { - alert('Can\'t read the clipboard, please contact the developer via email to returnzhy1996@outlook.com') + alert(`Can\'t read the clipboard, please contact the developer via email to ${AUTHOR_EMAIL}`) return } diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index 20b3b8a96..ffcee3c0f 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -16,6 +16,7 @@ import { defineComponent, ref, h } from "vue" import { renderOptionItem, tooltip } from "../../common" import BackUpAutoInput from "./auto-input" import Footer from "./footer" +import { AUTHOR_EMAIL } from "@src/package" const ALL_TYPES: timer.backup.Type[] = [ 'none', @@ -128,7 +129,7 @@ const _default = defineComponent({ h(ElAlert, { closable: false, type: "warning", - description: t(msg => msg.option.backup.alert) + description: t(msg => msg.option.backup.alert, { email: AUTHOR_EMAIL }) }), h(ElDivider), renderOptionItem({ diff --git a/src/app/components/rule-merge/components/item.ts b/src/app/components/rule-merge/components/item.ts index 59de3d592..7238606a2 100644 --- a/src/app/components/rule-merge/components/item.ts +++ b/src/app/components/rule-merge/components/item.ts @@ -4,23 +4,16 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { MergeTagType } from "@util/merge" +import type { Ref } from "vue" import { t } from "@app/locale" import { Edit } from "@element-plus/icons-vue" import { tryParseInteger } from "@util/number" import { ElTag } from "element-plus" -import { computed, defineComponent, h, ref, Ref, watch } from "vue" +import { computed, defineComponent, h, ref, watch } from "vue" import ItemInput from "./item-input" - -function computeType(mergedVal: number | string): '' | 'info' | 'success' { - return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' -} - -function computeTxt(mergedVal: number | string) { - return typeof mergedVal === 'number' - ? t(msg => msg.mergeRule.resultOfLevel, { level: mergedVal + 1 }) - : mergedVal === '' ? t(msg => msg.mergeRule.resultOfOrigin) : mergedVal -} +import { computeMergeTxt, computeMergeType } from "@util/merge" const _default = defineComponent({ name: "MergeRuleItem", @@ -47,8 +40,10 @@ const _default = defineComponent({ const id: Ref = ref(props.index || 0) watch(() => props.index, newVal => id.value = newVal) const editing: Ref = ref(false) - const type: Ref<'' | 'info' | 'success'> = computed(() => computeType(merged.value)) - const txt: Ref = computed(() => computeTxt(merged.value)) + const type: Ref = computed(() => computeMergeType(merged.value)) + const tagTxt: Ref = computed(() => computeMergeTxt(origin.value, merged.value, + (finder, param) => t(msg => finder(msg.mergeCommon), param) + )) ctx.expose({ forceEdit() { editing.value = true @@ -77,7 +72,10 @@ const _default = defineComponent({ type: type.value, closable: true, onClose: () => ctx.emit("delete", origin.value) - }, () => [`${origin.value} >>> ${txt.value}`, h(Edit, { class: "edit-icon", onclick: () => editing.value = true })]) + }, () => [ + tagTxt.value, + h(Edit, { class: "edit-icon", onclick: () => editing.value = true }) + ]) } }) diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 624db8210..7b4a08436 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -7,12 +7,13 @@ import type { UnwrapRef } from "vue" import type ElementIcon from "@src/element-ui/icon" +import type { MenuItemRegistered } from "element-plus" import type { RouteLocationNormalizedLoaded, Router } from "vue-router" import type { I18nKey } from "@app/locale" import type { MenuMessage } from "@i18n/message/app/menu" import { defineComponent, h, onMounted, reactive } from "vue" -import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" +import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup } from "element-plus" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/constant/url" @@ -20,7 +21,8 @@ import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Memo, Rank, Se import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" import { createTab } from "@api/chrome/tab" -import { ANALYSIS_ROUTE } from "@app/router/constants" +import { ANALYSIS_ROUTE, MERGE_ROUTE } from "@app/router/constants" +import { START_ROUTE } from "@guide/router/constants" type _MenuItem = { title: keyof MenuMessage @@ -46,7 +48,7 @@ type _RouteProps = { function generateMenus(): _MenuGroup[] { const otherMenuItems: _MenuItem[] = [{ title: 'userManual', - href: getGuidePageUrl(false), + href: getGuidePageUrl(false, START_ROUTE), icon: Memo, index: '_guide', }, { @@ -111,7 +113,7 @@ function generateMenus(): _MenuGroup[] { icon: Tickets }, { title: 'mergeRule', - route: '/additional/rule-merge', + route: MERGE_ROUTE, icon: Rank }, { title: 'option', @@ -135,8 +137,8 @@ function openMenu(route: string, title: I18nKey, routeProps: UnwrapRef<_RoutePro const openHref = (href: string) => createTab(href) -function handleClick(_MenuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { - const { route, title, href } = _MenuItem +function handleClick(menuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { + const { route, title, href } = menuItem if (route) { openMenu(route, msg => msg.menu[title], routeProps) } else { diff --git a/src/app/router/constants.ts b/src/app/router/constants.ts index 242c15216..984beea02 100644 --- a/src/app/router/constants.ts +++ b/src/app/router/constants.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -18,4 +18,9 @@ export const LIMIT_ROUTE = '/behavior/limit' /** * @since 0.9.1 */ -export const REPORT_ROUTE = '/data/report' \ No newline at end of file +export const REPORT_ROUTE = '/data/report' + +/** + * @since 1.8.0 + */ +export const MERGE_ROUTE = '/additional/rule-merge' \ No newline at end of file diff --git a/src/app/router/index.ts b/src/app/router/index.ts index f11dd70e1..38fec026c 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -9,7 +9,7 @@ import type { App } from "vue" import type { RouteRecordRaw } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router" -import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" +import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE, MERGE_ROUTE } from "./constants" import metaService from "@service/meta-service" const dataRoutes: RouteRecordRaw[] = [ @@ -58,7 +58,7 @@ const additionalRoutes: RouteRecordRaw[] = [ path: '/additional/whitelist', component: () => import('../components/whitelist') }, { - path: '/additional/rule-merge', + path: MERGE_ROUTE, component: () => import('../components/rule-merge') }, { path: OPTION_ROUTE, diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 7e288d679..f3b337148 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -13,6 +13,7 @@ import { createTab } from "@api/chrome/tab" import { createContextMenu } from "@api/chrome/context-menu" import { getRuntimeId } from "@api/chrome/runtime" import { locale } from "@i18n" +import { START_ROUTE } from "@guide/router/constants" const APP_PAGE_URL = getAppPageUrl(true) @@ -64,7 +65,7 @@ const feedbackPageProps: ChromeContextMenuCreateProps = { const guidePageProps: ChromeContextMenuCreateProps = { id: getRuntimeId() + '_timer_menu_item_guide_link', title: titleOf('📖', t2Chrome(msg => msg.base.guidePage)), - onclick: () => createTab(getGuidePageUrl(true)), + onclick: () => createTab(getGuidePageUrl(true, START_ROUTE)), ...baseProps } diff --git a/src/element-ui/table.ts b/src/element-ui/table.ts new file mode 100644 index 000000000..3b557895f --- /dev/null +++ b/src/element-ui/table.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { TableColumnCtx } from "element-plus" + +export type ElTableRowScope = { + row: T +} + +export type ElTableSpanMethodProps = { + row: T + column: TableColumnCtx + rowIndex: number + columnIndex: number +} \ No newline at end of file diff --git a/src/guide/component/app.ts b/src/guide/component/app.ts new file mode 100644 index 000000000..e1b960c99 --- /dev/null +++ b/src/guide/component/app.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { MERGE_ROUTE, PRIVACY_ROUTE } from "@guide/router/constants" +import { p, ul, alert } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, + }, + next: { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, + title: msg => msg.app.title, + }, () => [ + p(msg => msg.app.p1), + ul( + [msg => msg.app.l1, { button: t(msg => msg.base.allFunction) }], + [msg => msg.app.l2, { button: t(msg => msg.base.allFunction) }], + ), + alert(msg => msg.app.p2, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/backup.ts b/src/guide/component/backup.ts new file mode 100644 index 000000000..2da3eb28e --- /dev/null +++ b/src/guide/component/backup.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE } from "@guide/router/constants" +import { p, ul, h2, alert, appLink, link } from "./common/util" +import { OPTION_ROUTE, REPORT_ROUTE } from "@app/router/constants" +import { t } from "@guide/locale" +import { ElButton } from "element-plus" +import { UploadFilled } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.backup.title, + }, () => [ + p(msg => msg.backup.p1, { link: link('https://gist.github.com') }), + h2(msg => msg.backup.upload.title), + ul( + [msg => msg.backup.upload.prepareToken, { link: link('https://github.com/settings/tokens') }], + [msg => msg.backup.upload.enter, { link: appLink(OPTION_ROUTE, { i: 'backup' }) }], + msg => msg.backup.upload.form, + msg => msg.backup.upload.backup, + ), + h2(msg => msg.backup.query.title), + p(msg => msg.backup.query.p1), + ul( + [msg => msg.backup.query.enter, { + link: appLink(REPORT_ROUTE), + menuItem: t(msg => msg.appMenu.dataReport), + }], [msg => msg.backup.query.enable, { + icon: h(ElButton, { + type: 'text', + link: true, + icon: UploadFilled, + disabled: true + }) + }], + msg => msg.backup.query.wait, + ), + alert(msg => msg.backup.query.tip, 'warning'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts deleted file mode 100644 index 33e4de794..000000000 --- a/src/guide/component/common.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { I18nKey } from "@guide/locale" -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)) -} - -export function h2(i18nKey: I18nKey, position: Position): VNode { - return h('h2', { class: `guide-h2 ${position2AnchorClz(position)}` }, t(i18nKey)) -} - -export function paragraph(i18nKey: I18nKey, param?: any): VNode { - return h('div', { class: 'guide-paragragh' }, tN(i18nKey, param)) -} - -export function link(href: string, text: string): VNode { - return h('a', { class: 'guide-link', href, target: "_blank" }, text) -} - -export function linkInner(extensionUrl: string, text: string): VNode { - return h('a', { - class: 'guide-link', - onClick: () => createTab(extensionUrl), - }, text) -} - -export function list(...items: (I18nKey | [I18nKey, any])[]): VNode { - const children = items.map(item => { - let param = undefined - let i18nKey: I18nKey = undefined - if (Array.isArray(item)) { - [i18nKey, param] = item - } else { - i18nKey = item - } - return h('li', { class: 'guide-list-item' }, tN(i18nKey, param)) - }) - return h('ul', { class: 'guide-list' }, children) -} - -export function section(...vnodes: VNode[]): VNode { - return h('section', { class: 'guide-area' }, vnodes) -} \ No newline at end of file diff --git a/src/guide/component/common/article.sass b/src/guide/component/common/article.sass new file mode 100644 index 000000000..85b5993dc --- /dev/null +++ b/src/guide/component/common/article.sass @@ -0,0 +1,72 @@ +$footerH: 60px +$titleColor: #222 +$titleColorDark: #DDD +.article-container + margin: 0 15% 50px 15% + font-size: 15px + line-height: 1.5em + font-family: Segoe UI,SegoeUI,Helvetica Neue,Helvetica,Arial,sans-serif + h1, h2 + color: var(--guide-article-title-color) + .article-title + margin-top: 0px + margin-bottom: 32px + font-weight: 600 + font-size: 40px + line-height: 1.2em + .article-content + padding-bottom: 60px + text-align: justify + p, ul + color: var(--el-text-color-primary) + font-size: 16px + line-height: 1.6 + letter-spacing: 0.03em + ul + margin: 16px 0 16px 38px + padding: 0px + li + list-style: disc + h2 + font-size: 28px + line-height: 2 + font-weight: 700 + margin: 1.2em 0 0.8em + .img-container + width: 100% + text-align: center + .el-alert + margin: 30px 0 + padding: 12px 16px + border-radius: 6pxs + .el-alert__icon.is-big + font-size: var(--el-alert-icon-size) + width: var(--el-alert-icon-size) + .el-table__cell + .el-button.is-link + padding: 0 + .article-footer + height: $footerH + display: block + width: 100% + border-top: var(--el-border) + color: var(--el-text-color-primary) + .previous-container,.next-container + height: 100% + display: inline-flex + box-sizing: content-box + font-size: 18px + line-height: $footerH + vertical-align: middle + cursor: pointer + svg + margin: auto 5px + height: 18px + width: 18px + .next-container + float: right + +// Dark Mode +html[data-theme='dark'] + .article-container img + filter: brightness(0.85) saturate(1.25) diff --git a/src/guide/component/common/article.ts b/src/guide/component/common/article.ts new file mode 100644 index 000000000..4d7c936d7 --- /dev/null +++ b/src/guide/component/common/article.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { PropType } from "vue" +import type { Router } from "vue-router" + +import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" + +type Link = { + route: string + title: I18nKey +} + +function renderFooter(previous: Link, next: Link, router: Router) { + return h('div', { class: 'article-footer' }, [ + previous && h('div', { + class: 'previous-container', onClick: () => router.push(previous.route) + }, [ + h(ArrowLeft), + h('span', t(previous.title)), + ]), + next && h('div', { class: 'next-container', onClick: () => router.push(next.route) }, [ + h('span', t(next.title)), + h(ArrowRight), + ]) + ]) +} + +const _default = defineComponent({ + props: { + title: { + type: Function as PropType, + required: true, + }, + next: Object as PropType, + previous: Object as PropType, + }, + setup(props, ctx) { + const { previous, next, title } = props + const router = useRouter() + const content = ctx.slots.default + return () => h('div', { class: 'article-container' }, [ + h('h1', { class: 'article-title' }, t(title)), + content && h('div', { class: 'article-content' }, h(content)), + (previous || next) && renderFooter(previous, next, router) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common/util.ts b/src/guide/component/common/util.ts new file mode 100644 index 000000000..e02928d76 --- /dev/null +++ b/src/guide/component/common/util.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { getUrl } from "@api/chrome/runtime" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { Link } from "@element-plus/icons-vue" +import { t, tN } from "@guide/locale" +import { getAppPageUrl } from "@util/constant/url" +import { ElAlert, ElButton } from "element-plus" +import { h } from "vue" + +/** + * paragraph + */ +export function p(i18nKey: I18nKey, param?: any): VNode { + return h('p', tN(i18nKey, param)) +} + +/** + * Subtitle + */ +export function h2(i18nKey: I18nKey, param?: any): VNode { + return h('h2', tN(i18nKey, param)) +} + +/** + * Image + */ +export function img(fileName: string, param?: { width?: number, height?: number }): VNode { + return h('div', { class: 'img-container' }, + h('img', { ...(param || {}), src: getUrl(`static/images/guide/${fileName}`) }) + ) +} + +/** + * Alert + */ +export function alert(i18nKey: I18nKey, type: 'success' | 'warning' | 'info' = 'info', param?: any): VNode { + return h(ElAlert, { + type, + closable: false, + showIcon: true, + }, () => t(i18nKey, param)) +} + +/** + * ul + */ +export function ul(...liKeys: ([I18nKey, any] | I18nKey)[]): VNode { + return h('ul', { class: 'list-container' }, + liKeys.map(liKey => { + let i18nKey: I18nKey = undefined, param = undefined + if (typeof liKey === 'function') { + i18nKey = liKey + } else { + i18nKey = liKey[0] + param = liKey[1] + } + return h('li', tN(i18nKey, param)) + }) + ) +} + +export const appLink = (route?: string, query?: any) => link(getAppPageUrl(false, route, query)) + +export const link = (url: string) => h(ElButton, { + link: true, + icon: Link, + type: 'primary', + onClick: () => createTabAfterCurrent(url) +}) \ No newline at end of file diff --git a/src/guide/component/home/download-button.ts b/src/guide/component/home/download-button.ts new file mode 100644 index 000000000..13e4bea5b --- /dev/null +++ b/src/guide/component/home/download-button.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { createTabAfterCurrent } from "@api/chrome/tab" +import { ArrowDown, Download } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { IS_CHROME, IS_EDGE, IS_FIREFOX } from "@util/constant/environment" +import { CHROME_HOMEPAGE, EDGE_HOMEPAGE, FIREFOX_HOMEPAGE } from "@util/constant/url" +import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus" +import { defineComponent, h } from "vue" + +type _Info = { + name: string + url: string +} + +function allInfo(): _Info[] { + const result: _Info[] = [] + !IS_CHROME && result.push({ name: 'Chrome', url: CHROME_HOMEPAGE }) + !IS_EDGE && result.push({ name: 'Edge', url: EDGE_HOMEPAGE }) + !IS_FIREFOX && result.push({ name: 'Firefox', url: FIREFOX_HOMEPAGE }) + return result +} + +function openUrl(url: string) { + if (!url) return + createTabAfterCurrent(url) +} + +function i18nOfBtn(name: string) { + return t(msg => msg.home.download, { browser: name }) +} + +const _default = defineComponent(() => { + const infos = allInfo() + const firstInfo = infos[0] + const dropdownInfos = infos.slice(1) + return () => h('div', { class: 'download-container' }, [ + firstInfo && h(ElButton, { icon: Download, onClick: () => openUrl(firstInfo.url) }, () => i18nOfBtn(firstInfo.name)), + dropdownInfos?.length && h(ElDropdown, { + onCommand: openUrl, + trigger: 'hover' + }, { + default: () => h(ElIcon, {}, () => h(ArrowDown)), + dropdown: () => h(ElDropdownMenu, {}, () => dropdownInfos + .map(info => h(ElDropdownItem, { command: info.url }, () => i18nOfBtn(info.name))) + ) + }) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/home.sass b/src/guide/component/home/home.sass new file mode 100644 index 000000000..89a867206 --- /dev/null +++ b/src/guide/component/home/home.sass @@ -0,0 +1,45 @@ +.home-container + margin-top: 50px + margin-bottom: 40px + text-align: center + .slogan + font-size: 48px + line-height: 1.25em + font-weight: 600 + margin-bottom: 0.5em + color: var(--el-text-color-primary) + .desc + font-size: 18px + line-height: 1.75em + margin-bottom: 24px + font-weight: 400 + color: var(--el-text-color-regular) + max-width: 55% + margin: 0 auto + padding-bottom: 24px + .button-container + margin-top: 20px + font-size: 16px + font-weight: 400px + .el-button + height: 46px + .download-container + margin-left: 30px + display: inline-block + .el-button + border-top-right-radius: 0 + border-bottom-right-radius: 0 + .el-dropdown + vertical-align: middle + height: 44px + border: var(--el-border) + border-left: none + border-top-right-radius: var(--el-border-radius-base) + border-bottom-right-radius: var(--el-border-radius-base) + background-color: var(--el-border-color) + .el-icon + margin: auto 2px + .el-icon:focus-visible + outline: none + img + width: 23% diff --git a/src/guide/component/home/index.ts b/src/guide/component/home/index.ts new file mode 100644 index 000000000..3dbca1bae --- /dev/null +++ b/src/guide/component/home/index.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import DownloadButton from "./download-button" +import StartButton from "./start-button" +import './home.sass' + +const PIC_URL = chrome.runtime.getURL("static/images/guide/home.png") + +const _default = defineComponent(() => { + return () => h('div', { class: 'home-container' }, [ + h('h1', { class: 'slogan' }, t(msg => msg.meta.slogan)), + h('img', { src: PIC_URL }), + h('p', { class: 'desc' }, t(msg => msg.home.desc)), + h('div', { class: 'button-container' }, [ + h(StartButton), + h(DownloadButton) + ]), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/start-button.ts b/src/guide/component/home/start-button.ts new file mode 100644 index 000000000..95293b894 --- /dev/null +++ b/src/guide/component/home/start-button.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { START_ROUTE } from "@guide/router/constants" +import { ElButton } from "element-plus" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" +import { Document } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const router = useRouter() + return () => h(ElButton, { + class: 'start-button', + type: 'primary', + onClick: () => router.push(START_ROUTE), + icon: Document, + }, () => t(msg => msg.home.button)) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/limit.ts b/src/guide/component/limit.ts new file mode 100644 index 000000000..ec4aeaa76 --- /dev/null +++ b/src/guide/component/limit.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { BACKUP_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title + }, + next: { + route: BACKUP_ROUTE, + title: msg => msg.backup.title, + }, + title: msg => msg.limit.title, + }, () => [ + p(msg => msg.limit.p1), + h2(msg => msg.limit.step.title), + ul( + [msg => msg.limit.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.limit) }], + msg => msg.limit.step.click, + msg => msg.limit.step.form, + msg => msg.limit.step.check, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/index.ts b/src/guide/component/merge/index.ts new file mode 100644 index 000000000..cf53c963c --- /dev/null +++ b/src/guide/component/merge/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { APP_PAGE_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, appLink, } from "../common/util" +import { MERGE_ROUTE } from "@app/router/constants" +import { renderTargetTable, renderSiteExampleTable } from "./target-table" +import { renderSourceTable } from "./source-table" +import { renderRuleTag } from "./rule-tag" +import "./merge.sass" + +const renderDemo = () => renderRuleTag('gist.github.com', 'github.com') + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: APP_PAGE_ROUTE, + title: msg => msg.app.title + }, + next: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, + title: msg => msg.merge.title, + }, () => [ + p(msg => msg.merge.p1, { + demo1: h('i', 'www.github.com'), + demo2: h('i', 'gist.github.com'), + }), + p(msg => msg.merge.p2, { link: appLink(MERGE_ROUTE) }), + h2(msg => msg.merge.lookTitle), + p(msg => msg.merge.p3, { demo: renderDemo() }), + h2(msg => msg.merge.source.title), + p(msg => msg.merge.source.p1), + renderSourceTable(), + h2(msg => msg.merge.target.title), + p(msg => msg.merge.target.p1), + renderTargetTable(), + p(msg => msg.merge.target.p2), + renderSiteExampleTable(), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/merge.sass b/src/guide/component/merge/merge.sass new file mode 100644 index 000000000..954eabab7 --- /dev/null +++ b/src/guide/component/merge/merge.sass @@ -0,0 +1,8 @@ +.source-example-cell + display: flex + justify-content: space-between + word-wrap: break-word + .el-tag:last-child + margin-right: auto + .el-tag:not(last-child) + margin-right: 10px diff --git a/src/guide/component/merge/rule-tag.ts b/src/guide/component/merge/rule-tag.ts new file mode 100644 index 000000000..92923eb4e --- /dev/null +++ b/src/guide/component/merge/rule-tag.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { computeMergeTxt, computeMergeType } from "@util/merge" +import { ElTag } from "element-plus" +import { h } from "vue" + +export const renderRuleTag = (origin: string, merged: string | number) => { + const mergedVal = merged ?? '' + return h(ElTag, { + type: computeMergeType(mergedVal), + size: 'small', + }, () => computeMergeTxt(origin, mergedVal, (finder, param) => t(msg => finder(msg.mergeCommon), param))) +} diff --git a/src/guide/component/merge/source-table.ts b/src/guide/component/merge/source-table.ts new file mode 100644 index 000000000..64ff6ca1f --- /dev/null +++ b/src/guide/component/merge/source-table.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" + +import { t, tN } from "@guide/locale" +import { ElTableRowScope } from "@src/element-ui/table" +import { ElTable, ElTableColumn, ElTag } from "element-plus" +import { h } from "vue" + +type _SourceExample = { + source: string + examples: string[] | I18nKey +} + +const renderSiteExample = (site: string) => h(ElTag, { type: 'info', size: 'small' }, () => site) + +const SOURCE_EXAMPLES: _SourceExample[] = [{ + source: 'www.google.com', + examples: msg => msg.merge.source.only, +}, { + source: 'www.google.com.*', + examples: ['www.google.com.hk', 'www.google.com.au'], +}, { + source: '**.mit.edu', + examples: ['www.mit.edu', 'libraries.mit.edu', 'web.mit.edu', 'foo.bar.mit.edu'], +}] + +const renderSourceExample = (row: _SourceExample) => { + const { source, examples } = row + if (typeof examples === 'function') { + return h('span', tN(examples, { source: renderSiteExample(source) })) + } + const exampleTags = examples.map(renderSiteExample) + return h('div', { class: 'source-example-cell' }, exampleTags) +} + +export const renderSourceTable = () => h(ElTable, { + data: SOURCE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.merge.sourceCol), + width: 240, + }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.source.exampleCol) }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => renderSourceExample(row) + }), +]) \ No newline at end of file diff --git a/src/guide/component/merge/target-table.ts b/src/guide/component/merge/target-table.ts new file mode 100644 index 000000000..de27b18d1 --- /dev/null +++ b/src/guide/component/merge/target-table.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { t, tN } from "@guide/locale" +import { ElTable, ElTableColumn } from "element-plus" +import { h } from 'vue' +import { renderRuleTag } from "./rule-tag" +import CustomizedHostMergeRuler from "@service/components/host-merge-ruler" +import { PSL_HOMEPAGE } from "@util/constant/url" +import { link } from "../common/util" + +type _RuleExample = { + source: string + target?: string | number +} + +type _TargetExample = _RuleExample & { + remark: I18nKey +} + +type _SiteExample = { + original: string + ruleIdx?: number | number[] + remark?: string | (VNode | string)[] +} + +function computeTarget(val: undefined | string | number): string | number { + return typeof val === 'number' ? val + 1 : (val || '') +} + +const renderTag = (rule: _RuleExample) => renderRuleTag(rule?.source, rule?.target) + +const TARGET_EXAMPLES: _TargetExample[] = [{ + source: 'www.google.com.*', + target: 'google.com', + remark: msg => msg.merge.target.remark.spec, +}, { + source: 'www.google.com.hk', + remark: msg => msg.merge.target.remark.blank +}, { + source: '**.*.google.com', + target: 2, + remark: msg => msg.merge.target.remark.integer +}] + +export const renderTargetTable = () => h(ElTable, { + data: TARGET_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { label: t(msg => msg.merge.sourceCol), width: 180 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.targetCol), width: 150 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => computeTarget(row.target) + }), + h(ElTableColumn, { label: t(msg => msg.merge.target.lookCol), width: 240 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => renderTag(row) + }), + h(ElTableColumn, { label: t(msg => msg.merge.remarkCol), minWidth: 300 }, { + default: ({ row: { source, target, remark } }: ElTableRowScope<_TargetExample>) => tN(remark, { + source: h('i', source), + target: h('i', computeTarget(target)), + }) + }), +]) + +const MERGER = new CustomizedHostMergeRuler(TARGET_EXAMPLES.map( + ({ source: origin, target: merged }) => ({ origin, merged }) +)) + +const SITE_EXAMPLES: _SiteExample[] = [{ + original: 'www.google.com.au', + ruleIdx: 0, +}, { + original: 'www.google.com.pt', + ruleIdx: 0, +}, { + original: 'www.google.com.hk', + ruleIdx: [0, 1], + remark: t(msg => msg.merge.target.remark.specFirst), +}, { + original: 'es.news.google.com', + ruleIdx: [2], +}, { + original: 'a.b.c.phontos.google.com', + ruleIdx: [2], +}, { + original: 'pass.hust.edu.cn', + remark: tN(msg => msg.merge.target.remark.miss, { + link: link(PSL_HOMEPAGE), + }), +}] + +function renderHitCell({ ruleIdx }: _SiteExample): string | VNode { + const idxType = typeof ruleIdx + if (idxType === 'undefined') { + return '' + } else if (idxType === 'number') { + const rule = TARGET_EXAMPLES[ruleIdx as number] + return rule ? renderTag(rule) : '' + } else { + return h('span', + (ruleIdx as number[]) + .map(idx => TARGET_EXAMPLES[idx]) + .filter(a => !!a) + .map(renderTag) + ) + } +} + +export const renderSiteExampleTable = () => h(ElTable, { + data: SITE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + width: 195, + label: t(msg => msg.merge.target.originalCol), + formatter: (row: _SiteExample) => row.original, + }), + h(ElTableColumn, { + width: 160, + label: t(msg => msg.merge.target.mergedCol), + formatter: (row: _SiteExample) => MERGER.merge(row.original) + }), + h(ElTableColumn, { + width: 235, + label: t(msg => msg.merge.target.hitCol), + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => renderHitCell(row) + }), + h(ElTableColumn, { + label: t(msg => msg.merge.remarkCol) + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => row.remark + }) +]) \ No newline at end of file diff --git a/src/guide/component/privacy.ts b/src/guide/component/privacy.ts deleted file mode 100644 index c64545097..000000000 --- a/src/guide/component/privacy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineComponent } from "vue" -import { h1, h2, list, paragraph, section } from "./common" - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.privacy.title, 'privacy'), - h2(msg => msg.layout.menu.privacy.scope, 'privacy.scope'), - paragraph(msg => msg.privacy.scope.p1), - list( - msg => msg.privacy.scope.l1, - msg => msg.privacy.scope.l2, - msg => msg.privacy.scope.l3, - ), - h2(msg => msg.layout.menu.privacy.storage, 'privacy.storage'), - paragraph(msg => msg.privacy.storage.p1), - paragraph(msg => msg.privacy.storage.p2), - paragraph(msg => msg.privacy.storage.p3), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/index.ts b/src/guide/component/privacy/index.ts new file mode 100644 index 000000000..022de5095 --- /dev/null +++ b/src/guide/component/privacy/index.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { p, h2, alert } from "../common/util" +import { ElIcon, ElTable, ElTableColumn } from "element-plus" +import { PrivacyMessage, Scope } from "@i18n/message/guide/privacy" +import { t } from "@guide/locale" +import { CircleCheck, Warning } from "@element-plus/icons-vue" +import './privacy.sass' +import { START_ROUTE, APP_PAGE_ROUTE } from "@guide/router/constants" + +type ScopeRow = keyof PrivacyMessage['scope']['rows'] + +const ALL_ROWS: ScopeRow[] = ['website', 'tab', 'clipboard'] + +const renderTable = () => h(ElTable, { + data: ALL_ROWS, + border: true, + fit: true, + cellClassName: 'scope-table-cell', +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.name), + minWidth: 200, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].name) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.usage), + minWidth: 500, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].usage) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.required), + width: 250 + }, { + default: ({ row }: ElTableRowScope) => { + const reason = t(msg => (msg.privacy.scope.rows[row] as Scope).optionalReason) + return h('span', { class: reason ? 'optional' : 'required' }, [ + h(ElIcon, () => h(reason ? Warning : CircleCheck)), + reason + ]) + } + }), +]) + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.privacy.title, + previous: { route: START_ROUTE, title: msg => msg.start.title }, + next: { route: APP_PAGE_ROUTE, title: msg => msg.app.title }, + }, () => [ + alert(msg => msg.privacy.alert, 'warning'), + h2(msg => msg.privacy.scope.title), + renderTable(), + h2(msg => msg.privacy.storage.title), + p(msg => msg.privacy.storage.p1), + p(msg => msg.privacy.storage.p2), + alert(msg => msg.privacy.storage.p3, 'info'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/privacy.sass b/src/guide/component/privacy/privacy.sass new file mode 100644 index 000000000..1599149b9 --- /dev/null +++ b/src/guide/component/privacy/privacy.sass @@ -0,0 +1,10 @@ +.el-table__row .cell .required svg + color: var(--el-color-success) +.el-table__row .cell .optional svg + color: var(--el-color-warning) + +.el-table__row .cell .el-icon + font-size: 16px + margin-right: 5px +.el-table .cell + word-break: normal !important diff --git a/src/guide/component/profile.ts b/src/guide/component/profile.ts deleted file mode 100644 index 9f1b6651e..000000000 --- a/src/guide/component/profile.ts +++ /dev/null @@ -1,32 +0,0 @@ - -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { t } from "@guide/locale" -import { EDGE_HOMEPAGE, CHROME_HOMEPAGE, FIREFOX_HOMEPAGE, SOURCE_CODE_PAGE } from "@util/constant/url" -import { defineComponent } from "vue" - -import { h1, paragraph, link, section } from "./common" - -const _default = defineComponent({ - name: 'GuideProfile', - setup() { - return () => section( - h1(msg => msg.layout.menu.profile, 'profile', { appName: t(msg => msg.meta.name) }), - paragraph(msg => msg.profile.p1, { - edge: link(EDGE_HOMEPAGE, 'Edge'), - chrome: link(CHROME_HOMEPAGE, 'Chrome'), - firefox: link(FIREFOX_HOMEPAGE, 'Firefox'), - github: link(SOURCE_CODE_PAGE, 'Github'), - appName: t(msg => msg.meta.name), - }), - paragraph(msg => msg.profile.p2), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/start.ts b/src/guide/component/start.ts new file mode 100644 index 000000000..4503ac286 --- /dev/null +++ b/src/guide/component/start.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { img, p, h2, alert } from "./common/util" +import { getUrl } from "@api/chrome/runtime" +import { PRIVACY_ROUTE } from "@guide/router/constants" + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.start.title, + next: { title: msg => msg.privacy.title, route: PRIVACY_ROUTE }, + }, () => [ + p(msg => msg.start.p1), + h2(msg => msg.start.s1), + p(msg => msg.start.s1p1), + img('pin.png', { height: 300 }), + h2(msg => msg.start.s2), + p(msg => msg.start.s2p1, { + demo: h('img', { src: getUrl('static/images/guide/beating.gif') }) + }), + h2(msg => msg.start.s3), + p(msg => msg.start.s3p1), + alert(msg => msg.start.alert, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts deleted file mode 100644 index b634e1032..000000000 --- a/src/guide/component/usage.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getAppPageUrl, PSL_HOMEPAGE } from "@util/constant/url" -import { defineComponent } from "vue" -import { h1, h2, paragraph, list, link, section, linkInner } from "./common" -import { t } from "../locale" - -const quickstart = () => [ - h2(msg => msg.layout.menu.usage.quickstart, 'usage.quickstart'), - paragraph(msg => msg.usage.quickstart.p1), - list( - msg => msg.usage.quickstart.l1, - msg => msg.usage.quickstart.l2, - msg => msg.usage.quickstart.l3, - ), - paragraph(msg => msg.usage.quickstart.p2), -] - -const backgroundPageUrl = getAppPageUrl(false) - -const background = () => [ - h2(msg => msg.layout.menu.usage.background, 'usage.background'), - paragraph(msg => msg.usage.background.p1, { - background: linkInner(backgroundPageUrl, t(msg => msg.usage.background.backgroundPage)) - }), - list( - [msg => msg.usage.background.l1, { allFunction: t(msg => msg.base.allFunction) }], - [msg => msg.usage.background.l2, { allFunction: t(msg => msg.base.allFunction) }], - ), - paragraph(msg => msg.usage.background.p2), -] - -const advanced = () => [ - h2(msg => msg.layout.menu.usage.advanced, 'usage.advanced'), - paragraph(msg => msg.usage.advanced.p1), - list( - msg => msg.usage.advanced.l1, - msg => msg.usage.advanced.l2, - msg => msg.usage.advanced.l3, - msg => msg.usage.advanced.l4, - [msg => msg.usage.advanced.l5, { - psl: link(PSL_HOMEPAGE, 'Public Suffix List') - }], - msg => msg.usage.advanced.l6, - msg => msg.usage.advanced.l7, - msg => msg.usage.advanced.l8, - ), -] - -const backup = () => [ - h2(msg => msg.layout.menu.usage.backup, 'usage.backup'), - paragraph(msg => msg.usage.backup.p1, { gist: link('https://gist.github.com/', 'Github Gist') }), - list( - [msg => msg.usage.backup.l1, { - token: link('https://github.com/settings/tokens', 'token') - }], - msg => msg.usage.backup.l2, - msg => msg.usage.backup.l3, - ), -] - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.usage.title, 'usage'), - ...quickstart(), - ...background(), - ...advanced(), - ...backup(), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/virtual.ts b/src/guide/component/virtual.ts new file mode 100644 index 000000000..014afc5e4 --- /dev/null +++ b/src/guide/component/virtual.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE, MERGE_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { ElTag } from "element-plus" +import { t } from "@guide/locale" + +const demoTag = (demoSite: string) => h(ElTag, { size: 'small', type: 'info' }, () => demoSite) + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: MERGE_ROUTE, + title: msg => msg.merge.title + }, + next: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.virtual.title, + }, () => [ + p(msg => msg.virtual.p1), + h2(msg => msg.virtual.step.title), + ul( + [msg => msg.virtual.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.siteManage) }], + msg => msg.virtual.step.click, + [msg => msg.virtual.step.form, { + demo1: demoTag('github.com/sheepzh'), + demo2: demoTag('github.com/sheepzh/timer/**'), + }], + msg => msg.virtual.step.browse, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/guide.d.ts b/src/guide/guide.d.ts deleted file mode 100644 index a14023b1c..000000000 --- a/src/guide/guide.d.ts +++ /dev/null @@ -1,14 +0,0 @@ - -/** - * The anchor of menu items - */ -declare type Position = - | 'profile' - | 'usage' - | 'usage.quickstart' - | 'usage.background' - | 'usage.advanced' - | 'usage.backup' - | 'privacy' - | 'privacy.scope' - | 'privacy.storage' \ No newline at end of file diff --git a/src/guide/index.ts b/src/guide/index.ts index 3334af4c8..839e4808b 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -1,30 +1,35 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { Language } from "element-plus/lib/locale" import "./style" -import 'element-plus/theme-chalk/index.css' - -import { initLocale } from "@i18n" +import ElementPlus from 'element-plus' +import { initLocale, locale } from "@i18n" import { t } from "./locale" -import { init as initTheme, toggle } from "@util/dark-mode" +import installRouter from "./router" import { createApp } from "vue" import Main from "./layout" -import optionService from "@service/option-service" +import 'element-plus/theme-chalk/index.css' + +const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> } = { + zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), + zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), + en: () => import('element-plus/lib/locale/lang/en'), + ja: () => import('element-plus/lib/locale/lang/ja') +} async function main() { - initTheme() - // Calculate the latest mode - optionService.isDarkMode().then(toggle) await initLocale() - const app = createApp(Main) - app.mount('#guide') + installRouter(app) document.title = t(msg => msg.base.guidePage) + ' | ' + t(msg => msg.meta.name) + locales[locale]?.()?.then(msg => app.use(ElementPlus, { locale: msg.default })) + app.mount('#guide') } main() diff --git a/src/guide/layout/content.ts b/src/guide/layout/content.ts deleted file mode 100644 index 91217a2f1..000000000 --- a/src/guide/layout/content.ts +++ /dev/null @@ -1,34 +0,0 @@ - -import { ElDivider } from "element-plus" -import { watch, defineComponent, h, onMounted, PropType } from "vue" -import Profile from "../component/profile" -import Usage from "../component/usage" -import Privacy from "../component/privacy" -import { position2AnchorClz } from "@guide/util" - -function scrollPosition(position: Position) { - document.querySelector(`.${position2AnchorClz(position)}`)?.scrollIntoView?.() -} - -const _default = defineComponent({ - name: 'GuideContent', - props: { - position: { - type: String as PropType, - required: false, - } - }, - setup(props) { - onMounted(() => scrollPosition(props.position)) - watch(() => props.position, newVal => newVal && scrollPosition(newVal)) - return () => [ - h(Profile), - h(ElDivider), - h(Usage), - h(ElDivider), - h(Privacy), - ] - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/layout/header/dark-switch.ts b/src/guide/layout/header/dark-switch.ts new file mode 100644 index 000000000..47ea08aed --- /dev/null +++ b/src/guide/layout/header/dark-switch.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { Ref } from "vue" + +import { defineComponent, ref, h } from "vue" +import { init as initTheme, toggle } from "@util/dark-mode" +import optionService from "@service/option-service" +import { ElSwitch } from "element-plus" +import { Moon, Sunrise } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const value: Ref = ref(initTheme()) + const handleChange = (newVal: boolean) => { + toggle(newVal) + value.value = newVal + } + // Calculate the latest mode + optionService.isDarkMode().then(handleChange) + return () => h(ElSwitch, { + modelValue: value.value, + inactiveIcon: Sunrise, + activeIcon: Moon, + inlinePrompt: true, + async onChange(newVal: boolean) { + handleChange(newVal) + const option = await optionService.getAllOption() + option.darkMode = newVal ? 'on' : 'off' + optionService.setAppearanceOption(option) + }, + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/header.sass b/src/guide/layout/header/header.sass new file mode 100644 index 000000000..c685da29e --- /dev/null +++ b/src/guide/layout/header/header.sass @@ -0,0 +1,71 @@ + +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +$logoSize: 42px +$iconSize: 24px +$localeIconSize: 19px + +.header-container + line-height: 60px + height: 60px + display: flex + justify-content: space-between + margin: 0 auto + .header-left + display: flex + box-sizing: content-box + cursor: pointer + .icon,.title + height: $logoSize + line-height: $logoSize + display: inline-block + margin: auto 0 + .icon + margin-right: 10px + width: $logoSize + .title + font-size: 20px + color: var(--el-text-color-primary) + .header-right + .el-switch__inner + svg path + fill: var(--el-color-white) + svg + width: $iconSize + height: $iconSize + path + fill: var(--el-text-color-regular) + .el-switch + --el-switch-off-color: var(--el-color-primary) + // = [light-theme] --el-text-color-regular + --el-switch-on-color: #606266 + margin-left: 10px + .el-switch__core + width: 45px + .icon-link + display: inline-flex + background-color: transparent + height: $iconSize + border: none + padding: 0px + .locale-select, .icon-link + vertical-align: middle + margin-left: 10px + .locale-select + .locale-button + display: flex + svg + width: $localeIconSize + height: $localeIconSize + span + font-size: calc( $localeIconSize - 3 ) + padding-left: 3px + line-height: $localeIconSize + color: var(--el-text-color-regular) + .locale-button:focus-visible + outline: none diff --git a/src/guide/layout/header/icon-button.ts b/src/guide/layout/header/icon-button.ts new file mode 100644 index 000000000..d6c3c8654 --- /dev/null +++ b/src/guide/layout/header/icon-button.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElTooltip } from "element-plus" +import { defineComponent, h } from "vue" +import SvgIcon from "./svg-icon" + +const _default = defineComponent({ + props: { + path: String, + tip: String, + href: String, + }, + setup(props) { + return () => h(ElTooltip, { + effect: 'dark', + placement: 'bottom', + content: props.tip, + offset: 5, + showArrow: false, + }, () => h('a', { + href: props.href, + target: '_blank', + class: 'icon-link', + }, h(SvgIcon, { path: props.path }))) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/index.ts b/src/guide/layout/header/index.ts new file mode 100644 index 000000000..abcb1cc48 --- /dev/null +++ b/src/guide/layout/header/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import IconButton from "./icon-button" +import DarkSwitch from "./dark-switch" +import LocaleSelect from "./locale-select" +import './header.sass' +import { GITHUB_PATH, EMAIL_PATH } from "./svg" +import { useRouter } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" +import { SOURCE_CODE_PAGE } from "@util/constant/url" +import { AUTHOR_EMAIL } from "@src/package" + +const ICON_URL = chrome.runtime.getURL("static/images/icon.png") + +const logo = () => h('span', { class: 'icon' }, h('img', { src: ICON_URL, width: 42, height: 42 })) +const title = () => h('span', { class: 'title' }, t(msg => msg.meta.marketName)) +const github = () => h(IconButton, { + path: GITHUB_PATH, + tip: t(msg => msg.layout.header.sourceCode), + href: SOURCE_CODE_PAGE, +}) +const email = () => h(IconButton, { + path: EMAIL_PATH, + tip: t(msg => msg.layout.header.email), + href: `mailto:${AUTHOR_EMAIL}` +}) +const darkSwitch = () => h(DarkSwitch) +const localeSelect = () => h(LocaleSelect) + +const _default = defineComponent(() => { + const router = useRouter() + return () => h('div', { class: 'header-container' }, [ + h('div', { class: 'header-left', onClick: () => router.push(HOME_ROUTE) }, [logo(), title()]), + h('div', { class: 'header-right' }, [ + github(), + email(), + darkSwitch(), + localeSelect(), + ]) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/locale-select.ts b/src/guide/layout/header/locale-select.ts new file mode 100644 index 000000000..d7cb5e97e --- /dev/null +++ b/src/guide/layout/header/locale-select.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus" +import { defineComponent, h } from "vue" +import { getI18nVal, locale as current } from "@i18n" +import localeMessages from "@i18n/message/common/locale" +import SvgIcon from "./svg-icon" +import { LOCALE_PATH } from "./svg" +import optionService from "@service/option-service" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { CROWDIN_HOMEPAGE } from "@util/constant/url" + +const HELP_CMD: string = '_help' +const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja'] + +const getLocaleName = (locale: timer.Locale) => getI18nVal(localeMessages, msg => msg.name, locale) + +const renderIcon = () => h('div', + { class: 'locale-button' }, + [ + h(SvgIcon, { path: LOCALE_PATH }), + h('span', getLocaleName(current)) + ] +) + +const renderLocaleItem = (locale: timer.Locale) => h(ElDropdownItem, { + disabled: current === locale, + command: locale, +}, () => getLocaleName(locale)) + +const renderHelpItem = () => h(ElDropdownItem, { + divided: true, + command: HELP_CMD, +}, () => 'Help translate!') + +const renderMenuItems = () => h(ElDropdownMenu, + () => [ + ...ALL_LOCALES.map(renderLocaleItem), + renderHelpItem(), + ] +) + +const handleCommand = async (cmd: string) => { + if (cmd === HELP_CMD) { + createTabAfterCurrent(CROWDIN_HOMEPAGE) + return + } + const locale = cmd as timer.Locale + const option = await optionService.getAllOption() + option.locale = locale + await optionService.setAppearanceOption(option) + window.location.reload?.() +} + +const _default = defineComponent(() => { + return () => h(ElDropdown, { + class: 'locale-select', + onCommand: handleCommand + }, { + default: () => renderIcon(), + dropdown: () => renderMenuItems() + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg-icon.ts b/src/guide/layout/header/svg-icon.ts new file mode 100644 index 000000000..2511e6e52 --- /dev/null +++ b/src/guide/layout/header/svg-icon.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" + +const _default = defineComponent({ + props: { + path: String, + }, + setup(props) { + return () => h('svg', { + viewBox: "0 0 1024 1024", + xmlns: "http://www.w3.org/2000/svg" + }, h("path", { d: props.path })) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg.ts b/src/guide/layout/header/svg.ts new file mode 100644 index 000000000..4fa03b3ff --- /dev/null +++ b/src/guide/layout/header/svg.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const GITHUB_PATH = 'M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z' +export const EMAIL_PATH = 'M853.333333 170.666667H170.666667c-47.146667 0-84.906667 38.186667-84.906667 85.333333L85.333333 768c0 47.146667 38.186667 85.333333 85.333334 85.333333h682.666666c47.146667 0 85.333333-38.186667 85.333334-85.333333V256c0-47.146667-38.186667-85.333333-85.333334-85.333333z m0 170.666666L512 554.666667 170.666667 341.333333v-85.333333l341.333333 213.333333 341.333333-213.333333v85.333333z' +export const LOCALE_PATH = 'M540.245333 645.888L426.410667 522.069333l1.365333-1.536a889.770667 889.770667 0 0 0 166.314667-322.048h131.413333V99.669333H411.562667V1.109333h-89.6v98.645334H7.850667v98.730666h500.906666a797.696 797.696 0 0 1-142.165333 263.936 778.581333 778.581333 0 0 1-103.594667-165.290666h-89.6c33.621333 82.176 78.677333 158.037333 133.546667 224.938666L78.848 769.706667l63.658667 69.973333 224.170666-246.613333 139.52 153.429333 34.133334-100.608z m252.501334-250.026667H703.061333l-201.813333 591.872h89.685333l50.261334-147.968h212.992l50.688 147.968h89.685333L792.746667 395.776zM675.242667 741.034667l72.618666-213.674667 72.704 213.674667H675.242667z' diff --git a/src/guide/layout/index.ts b/src/guide/layout/index.ts index 7cec2f643..fa3ab421b 100644 --- a/src/guide/layout/index.ts +++ b/src/guide/layout/index.ts @@ -1,28 +1,28 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { Ref } from "vue" - -import { ElContainer, ElAside, ElMain } from "element-plus" -import { defineComponent, ref, h } from "vue" -import Content from "./content" +import { ElContainer, ElAside, ElMain, ElHeader, ElScrollbar } from "element-plus" +import { defineComponent, h } from "vue" +import Header from "./header" import Menu from "./menu" +import { RouterView, useRoute } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" + +const renderMain = () => h(ElContainer, {}, () => [ + h(ElAside, () => h(ElScrollbar, () => h(Menu))), + h(ElMain, () => h(ElScrollbar, () => h(RouterView))), +]) -const _default = defineComponent({ - name: "Guide", - render() { - const position: Ref = ref() - return h(ElContainer, { class: 'guide-container' }, () => [ - h(ElAside, {}, () => h(Menu, { onClick: (newPosition: Position) => position.value = newPosition })), - h(ElContainer, { - id: 'app-body' - }, () => h(ElMain, {}, () => h(Content, { position: position.value }))) - ]) - } +const _default = defineComponent(() => { + const route = useRoute() + return () => h(ElContainer, { class: 'guide-container' }, () => [ + h(ElHeader, () => h(Header)), + route.path === HOME_ROUTE ? h(RouterView) : renderMain(), + ]) }) export default _default \ No newline at end of file diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 21db6fd1c..1688faae2 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -5,118 +5,87 @@ * https://opensource.org/licenses/MIT */ -import { ref, VNode } from "vue" -import ElementIcon from "@src/element-ui/icon" -import { I18nKey, t } from "@guide/locale" -import { ElIcon, ElMenu, ElMenuItem, ElSubMenu } from "element-plus" -import { defineComponent, h } from "vue" -import { User, Memo, MagicStick } from "@element-plus/icons-vue" +import type { Ref } from "vue" +import type { I18nKey } from "@guide/locale" -type _Item = { - title: I18nKey - position: Position -} +import { t } from "@guide/locale" +import { ElMenu, ElMenuItem, ElSubMenu } from "element-plus" +import { defineComponent, h, onMounted, ref } from "vue" +import { Router, useRouter } from "vue-router" +import { + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + BACKUP_ROUTE, + LIMIT_ROUTE, +} from "@guide/router/constants" -type _Group = { +type MenuConf = { + route: string title: I18nKey - position: Position - children: _Item[] - icon: ElementIcon + children?: MenuConf[] } -const quickstartPosition: Position = 'usage.quickstart' -const profilePosition: Position = 'profile' -const menus: _Group[] = [ - { - title: msg => msg.layout.menu.usage.title, - position: 'usage', - children: [ - { - title: msg => msg.layout.menu.usage.quickstart, - position: quickstartPosition - }, { - title: msg => msg.layout.menu.usage.background, - position: 'usage.background' - }, { - title: msg => msg.layout.menu.usage.advanced, - position: 'usage.advanced' - }, { - title: msg => msg.layout.menu.usage.backup, - position: 'usage.backup', - } - ], - icon: Memo - }, - { - title: msg => msg.layout.menu.privacy.title, - position: 'privacy', - icon: User, - children: [ - { - title: msg => msg.layout.menu.privacy.scope, - position: 'privacy.scope' - }, - { - title: msg => msg.layout.menu.privacy.storage, - position: 'privacy.storage' - }, - ], +const MENU_CONFS: MenuConf[] = [{ + route: START_ROUTE, + title: msg => msg.start.title, +}, { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, +}, { + route: USAGE_ROUTE, + title: msg => msg.layout.menu.usage, + children: [{ + route: APP_PAGE_ROUTE, + title: msg => msg.app.title, + }, { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, { + route: LIMIT_ROUTE, + title: msg => msg.limit.title, + }, { + route: BACKUP_ROUTE, + title: msg => msg.backup.title + }], +}] +function renderWithConf(conf: MenuConf, router: Router, activeRoute: Ref) { + const { route, title, children } = conf + if (children?.length) { + return h(ElSubMenu, { + index: route, + }, { + title: () => h('span', t(title)), + default: () => children.map(child => renderWithConf(child, router, activeRoute)) + }) } -] - -function renderMenuItem(handleClick: (position: Position) => void, item: _Item, index: number): VNode { - const { title, position } = item return h(ElMenuItem, { - index: position, - onClick: () => handleClick(position) - }, () => h('span', {}, `${index + 1}. ${t(title)}`)) + index: route, + onClick: () => { + router.push(route) + activeRoute.value = route + }, + }, () => h('span', t(title))) } -function renderGroup(handleClick: (position: Position) => void, group: _Group): VNode { - const { position, title, children, icon } = group - return h(ElSubMenu, { - index: position, - }, { - title: () => [ - h(ElIcon, () => h(icon)), - h('span', {}, t(title)) - ], - default: () => children.map( - (item, index) => renderMenuItem(handleClick, item, index) - ) - }) -} - -const _default = defineComponent({ - name: "GuideMenu", - emits: { - click: (_position: Position) => true, - }, - setup(_, ctx) { - const handleClick = (position: Position) => ctx.emit('click', position) - const menuItems = () => [ - h(ElMenuItem, { - index: profilePosition, - onClick: () => handleClick(profilePosition) - }, () => [ - h(ElIcon, () => h(MagicStick)), - h('span', {}, t(msg => msg.layout.menu.profile, { appName: t(msg => msg.meta.name) })) - ]), - ...menus.map( - group => renderGroup(handleClick, group) - ) - ] - const menuRef = ref() - return () => h(ElMenu, { - defaultActive: profilePosition, - defaultOpeneds: menus.map(group => group.position), - ref: menuRef, - onClose(index: string) { - menuRef.value?.open?.(index) - } - }, menuItems) - } +const _default = defineComponent(() => { + const router = useRouter() + const activeRoute: Ref = ref() + // Initialize current route in a new macro task + onMounted(() => setTimeout(() => activeRoute.value = router.currentRoute?.value?.path)) + return () => [ + h(ElMenu, { + defaultOpeneds: MENU_CONFS.filter(m => m.children?.length).map(group => group.route), + defaultActive: activeRoute.value, + }, () => MENU_CONFS.map(conf => renderWithConf(conf, router, activeRoute))) + ] }) export default _default \ No newline at end of file diff --git a/src/guide/router/constants.ts b/src/guide/router/constants.ts new file mode 100644 index 000000000..484151ba8 --- /dev/null +++ b/src/guide/router/constants.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const HOME_ROUTE = '/home' +export const START_ROUTE = '/start' +export const PRIVACY_ROUTE = '/privacy' +export const USAGE_ROUTE = '/usage' +export const APP_PAGE_ROUTE = '/usage/management' +export const MERGE_ROUTE = '/usage/merge' +export const VIRTUAL_ROUTE = '/usage/virtual' +export const LIMIT_ROUTE = '/usage/limit' +export const BACKUP_ROUTE = '/usage/backup' \ No newline at end of file diff --git a/src/guide/router/index.ts b/src/guide/router/index.ts new file mode 100644 index 000000000..0c248b1cd --- /dev/null +++ b/src/guide/router/index.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { RouteRecordRaw } from "vue-router" + +import { App } from "vue" +import { createRouter, createWebHashHistory } from "vue-router" +import { + HOME_ROUTE, + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + LIMIT_ROUTE, + BACKUP_ROUTE, +} from "./constants" + + +const routes: RouteRecordRaw[] = [{ + path: '/', + redirect: HOME_ROUTE, +}, { + path: HOME_ROUTE, + component: () => import('../component/home'), +}, { + path: START_ROUTE, + component: () => import('../component/start'), +}, { + path: PRIVACY_ROUTE, + component: () => import('../component/privacy') +}, { + path: USAGE_ROUTE, + redirect: APP_PAGE_ROUTE, +}, { + path: APP_PAGE_ROUTE, + component: () => import('../component/app'), +}, { + path: MERGE_ROUTE, + component: () => import('../component/merge'), +}, { + path: VIRTUAL_ROUTE, + component: () => import('../component/virtual'), +}, { + path: LIMIT_ROUTE, + component: () => import('../component/limit'), +}, { + path: BACKUP_ROUTE, + component: () => import('../component/backup'), +}] + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +export default (app: App) => { + app.use(router) +} diff --git a/src/guide/style/dark-theme.sass b/src/guide/style/dark-theme.sass index 38b9ef1cf..d426eb9e7 100644 --- a/src/guide/style/dark-theme.sass +++ b/src/guide/style/dark-theme.sass @@ -77,4 +77,5 @@ html[data-theme='dark']:root --el-mask-color-extra-light: rgba(0 0 0 .3) --guide-container-bg-color: var(--el-fill-color-dark) // menu - --el-menu-bg-color: var(--el-fill-color-light) + --el-menu-bg-color: var(--guide-container-bg-color) + --guide-article-title-color: #DDDDDD diff --git a/src/guide/style/index.sass b/src/guide/style/index.sass index b1a9ffe1f..5acf906d8 100644 --- a/src/guide/style/index.sass +++ b/src/guide/style/index.sass @@ -7,6 +7,11 @@ @import './light-theme' @import './dark-theme' +@import '../component/common/article' + +$headerHeight: 61px +$contentHeight: calc(100vh - $headerHeight) +$asideWidth: 360px body margin: 0px @@ -15,31 +20,38 @@ body display: flex height: 100vh width: 100% - scroll-behavior: smooth + .el-header + position: relative + width: 100% + height: 60px + border-bottom: 1px var(--el-border-color) var(--el-border-style) + padding: 0 30px + .el-aside + display: block + position: absolute + left: 0 + top: $headerHeight + bottom: 0 + height: $contentHeight + width: $asideWidth + padding-top: 15px + padding-left: 15px + .el-menu + border-right: none .guide-container background: var(--guide-container-bg-color) - .el-menu - height: 100% + overflow-y: auto .el-main - height: 100% - padding: 10px 0 + position: absolute + left: $asideWidth + right: 0px + top: $headerHeight + bottom: 0 + padding: 30px 20px + // 以下待删除 .guide-area padding: 0 9vw padding-bottom: 10px - h1,h2,span,div,li - color: var(--el-text-color-primary) - h1 - font-weight: 500 - font-size: 1.8rem - letter-spacing: 0 - line-height: 1.333 - h2 - font-weight: 500 - font-size: 1.4rem - letter-spacing: 0 - line-height: 1.333 - margin: 40px 0 - margin-left: 2px .guide-paragragh,.guide-list font-size: 0.9375rem font-weight: 400 diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass index e75ed01cf..c806d1332 100644 --- a/src/guide/style/light-theme.sass +++ b/src/guide/style/light-theme.sass @@ -10,3 +10,4 @@ --el-menu-text-color: #c1c6c8 --el-menu-hover-bg-color: #262f3e --guide-container-bg-color: var(--el-fill-color-blank) + --guide-article-title-color: #222222 diff --git a/src/guide/util.ts b/src/guide/util.ts deleted file mode 100644 index 474f93566..000000000 --- a/src/guide/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function position2AnchorClz(position: Position): string { - return `anchor-${position?.replace('.', '-')}` -} \ No newline at end of file diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index 9ef326c60..7ba5c9ea8 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -51,6 +51,7 @@ const placeholder: ChromeMessage = { name: '', description: '', marketName: '', + slogan: '', }, base: { currentVersion: '', diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 5a7e79566..e95a1d2e2 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -1,11 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import itemMessages, { ItemMessage } from "@i18n/message/common/item" +import mergeCommonMessages, { MergeCommonMessage } from "@i18n/message/common/merge" import dataManageMessages, { DataManageMessage } from "./data-manage" import reportMessages, { ReportMessage } from "./report" import analysisMessages, { AnalysisMessage } from "./analysis" @@ -27,6 +28,7 @@ import helpUsMessages, { HelpUsMessage } from "./help-us" export type AppMessage = { dataManage: DataManageMessage item: ItemMessage + mergeCommon: MergeCommonMessage report: ReportMessage whitelist: WhitelistMessage mergeRule: MergeRuleMessage @@ -49,6 +51,7 @@ const _default: Messages = { zh_CN: { dataManage: dataManageMessages.zh_CN, item: itemMessages.zh_CN, + mergeCommon: mergeCommonMessages.zh_CN, report: reportMessages.zh_CN, whitelist: whitelistMessages.zh_CN, mergeRule: mergeRuleMessages.zh_CN, @@ -69,6 +72,7 @@ const _default: Messages = { zh_TW: { dataManage: dataManageMessages.zh_TW, item: itemMessages.zh_TW, + mergeCommon: mergeCommonMessages.zh_TW, report: reportMessages.zh_TW, whitelist: whitelistMessages.zh_TW, mergeRule: mergeRuleMessages.zh_TW, @@ -89,6 +93,7 @@ const _default: Messages = { en: { dataManage: dataManageMessages.en, item: itemMessages.en, + mergeCommon: mergeCommonMessages.en, report: reportMessages.en, whitelist: whitelistMessages.en, mergeRule: mergeRuleMessages.en, @@ -109,6 +114,7 @@ const _default: Messages = { ja: { dataManage: dataManageMessages.ja, item: itemMessages.ja, + mergeCommon: mergeCommonMessages.ja, report: reportMessages.ja, whitelist: whitelistMessages.ja, mergeRule: mergeRuleMessages.ja, diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index aee5d5e74..73d1aa7ce 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -65,7 +65,7 @@ const _default: Messages = { feedback: '有什麼反饋嗎?', rate: '打個分吧!', helpUs: '帮助我们~', - userManual: '用戶手冊', + userManual: '使用者手冊', }, en: { dashboard: 'Dashboard', diff --git a/src/i18n/message/app/merge-rule.ts b/src/i18n/message/app/merge-rule.ts index 87a91092f..544f0caa5 100644 --- a/src/i18n/message/app/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -1,13 +1,11 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ export type MergeRuleMessage = { - resultOfOrigin: string - resultOfLevel: string removeConfirmMsg: string originPlaceholder: string mergedPlaceholder: string @@ -25,8 +23,6 @@ export type MergeRuleMessage = { const _default: Messages = { zh_CN: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 级域名', removeConfirmMsg: '自定义合并规则 {origin} 将被移除', originPlaceholder: '原域名', mergedPlaceholder: '合并后域名', @@ -42,8 +38,6 @@ const _default: Messages = { infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级', }, zh_TW: { - resultOfOrigin: '不合並', - resultOfLevel: '{level} 級網域', removeConfirmMsg: '自定義合並規則 {origin} 將被移除', originPlaceholder: '原網域', mergedPlaceholder: '合並後網域', @@ -59,8 +53,6 @@ const _default: Messages = { infoAlert5: '如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級', }, en: { - resultOfOrigin: 'Not Merge', - resultOfLevel: 'Keep Level {level}', removeConfirmMsg: '{origin} will be removed from customized merge rules.', originPlaceholder: 'Origin site', mergedPlaceholder: 'Merged', @@ -76,8 +68,6 @@ const _default: Messages = { infoAlert5: 'If no rule is matched, it will default to the level before {psl}', }, ja: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 次ドメイン', removeConfirmMsg: 'カスタム マージ ルール {origin} は削除されます', originPlaceholder: '独自ドメイン名', mergedPlaceholder: '統計的ドメイン名', diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index dd34d377e..2c1ab322d 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -84,13 +84,6 @@ export type OptionMessage = { defaultValue: string } -const FOLLOW_BROWSER: Record = { - zh_CN: '跟随浏览器', - zh_TW: '跟隨瀏覽器', - en: 'Follow browser', - ja: 'ブラウザと同じ', -} - const _default: Messages = { zh_CN: { yes: '是', @@ -161,12 +154,12 @@ const _default: Messages = { auth: '', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '需要创建一个至少包含 gist 权限的 token', }, }, - alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', + alert: '这是一项实验性功能,如果有任何问题请联系作者~ ({email})', test: '测试', lastTimeTip: '上次备份时间: {lastTime}', operation: '备份数据', @@ -247,12 +240,12 @@ const _default: Messages = { label: '關閉備份', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '需要創建一個至少包含 gist 權限的 token', }, }, - alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', + alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 ({email}) ~', test: '測試', operation: '備份數據', lastTimeTip: '上次備份時間: {lastTime}', @@ -333,12 +326,12 @@ const _default: Messages = { label: 'Always off', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: 'One token with at least gist permission is required', }, }, - alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', + alert: 'This is an experimental feature, if you have any questions please contact the author via {email}~', test: 'Test', operation: 'Backup', lastTimeTip: 'Last backup time: {lastTime}', @@ -419,12 +412,12 @@ const _default: Messages = { label: 'バックアップを有効にしない', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '少なくとも gist 権限を持つトークンが 1 つ必要です', }, }, - alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', + alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください ({email})', test: 'テスト', operation: 'バックアップ', lastTimeTip: '前回のバックアップ時間: {lastTime}', diff --git a/src/i18n/message/common/base.ts b/src/i18n/message/common/base.ts index 042b44eeb..56d9331fe 100644 --- a/src/i18n/message/common/base.ts +++ b/src/i18n/message/common/base.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -28,7 +28,7 @@ const _default: Messages = { zh_TW: { currentVersion: '版本: v{version}', allFunction: '所有功能', - guidePage: '用戶手冊', + guidePage: '使用者手冊', }, ja: { currentVersion: 'バージョン: v{version}', diff --git a/src/i18n/message/common/merge.ts b/src/i18n/message/common/merge.ts new file mode 100644 index 000000000..3f056d4b4 --- /dev/null +++ b/src/i18n/message/common/merge.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type MergeCommonMessage = { + tagResult: { + blank: string + level: string + } +} + +const messages: Messages = { + en: { + tagResult: { + blank: 'Not Merge', + level: 'Keep Level {level}', + } + }, + zh_CN: { + tagResult: { + blank: '不合并', + level: '{level} 级域名', + }, + }, + zh_TW: { + tagResult: { + blank: '不合並', + level: '{level} 級網域', + } + }, + ja: { + tagResult: { + blank: '不合并', + level: '{level} 次ドメイン', + } + } +} + +export default messages \ No newline at end of file diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index 43b9b2b0e..c26eb7266 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -1,15 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ export type MetaMessage = { name: string marketName: string description: string + slogan: string } +const SLOGAN_EN = 'Insight & Improve' + const _default: Messages = { zh_CN: { name: '网费很贵', marketName: '网费很贵 - 上网时间统计', description: '做最好用的上网时间统计工具。', + slogan: SLOGAN_EN, }, zh_TW: { name: '網費很貴', @@ -25,6 +35,7 @@ const _default: Messages = { name: 'Timer', marketName: 'Timer - Browsing Time & Visit count', description: 'To be the BEST web timer.', + slogan: SLOGAN_EN, }, } diff --git a/src/i18n/message/guide/app.ts b/src/i18n/message/guide/app.ts new file mode 100644 index 000000000..a977f7054 --- /dev/null +++ b/src/i18n/message/guide/app.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type AppMessage = { + title: string + p1: string + l1: string + l2: string + p2: string +} + +const _default: Messages = { + en: { + title: 'Enter the management page', + p1: 'Based on icons, the extension provides a more convenient way to view data. ' + + 'But if you want to experience full functionality, ' + + 'you need to visit the management page of the extension, via one of the following two ways.', + l1: 'You can right-click the icon of the extension, and click [{button}] in the pop-up menu.', + l2: 'You can also find the [{button}] link at the bottom of the icon popup page, just click it.', + p2: 'The popup page and management page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', + }, + zh_CN: { + title: '进入后台管理页面', + p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的后台管理页。进入后台页有以下两种方式。', + l1: '您可以右击扩展的图标,在弹出的菜单中点击【{button}】。', + l2: '您也可以在图标弹出页的页脚找到【{button}】链接,同样地,点击它即可。', + p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。' + }, + zh_TW: { + title: '進入管理頁面', + p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的後台管理頁。進入後台頁有以下兩種方式。', + l1: '您可以右擊擴展的圖標,在彈出的菜單中點擊【{button}】。', + l2: '您也可以在圖標彈出頁的頁腳找到【{button}】鏈接,同樣地,點擊它即可。', + p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', + }, + ja: { + title: '管理ページに入る', + p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、その完全な機能を体験したい場合は、拡張バックグラウンド管理ページにアクセスする必要があります. バックグラウンド ページに入る方法は 2 つあります。', + l1: '拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。', + l2: 'アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。', + p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', + }, +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/backup.ts b/src/i18n/message/guide/backup.ts new file mode 100644 index 000000000..f21e5b49f --- /dev/null +++ b/src/i18n/message/guide/backup.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type BackupMessage = { + title: String + p1: string + upload: { + title: string + prepareToken: string + enter: string + form: string + backup: string + } + query: { + title: string + p1: string + enter: string + enable: string + wait: string + tip: string + } +} + +const _default: Messages = { + en: { + title: 'Backup data with GitHub Gist', + p1: 'This extension supports users to backup data using GitHub Gist{link} through simple settings.', + upload: { + title: 'Four simple steps to complete the setup', + prepareToken: 'First, generate a token with gist permissions on GitHub{link}.', + enter: 'Enter the option page{link}.', + form: 'Then select GitHub Gist as the synchronization method, ' + + 'and fill in your token in the input box that appears below.', + backup: 'Click the Backup button to upload local data to your GitHub Gist.', + }, + query: { + title: 'How to query data backed up by other browsers?', + p1: 'If you correctly set the token in the above steps, you can query remote data in just three simple steps.', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + enable: 'If the token is set correctly, an icon, like {icon}, will appear in the upper right corner of the page, ' + + 'click it to enable the remote query.', + wait: 'Wait for the data query to complete, and move the mouse over the value to view the data of each client.', + tip: 'Because remote data is stored in monthly shards, the query time period should not be too long.', + } + }, + zh_CN: { + title: '使用 GitHub Gist 备份数据', + p1: '这个扩展支持用户通过简单的设置,使用 GitHub Gist{link} 备份数据。', + upload: { + title: '简单四步完成设置', + prepareToken: '首先,您需要在 GitHub 生成一个包含 gist 权限的 token{link}。', + enter: '进入扩展的选项页面{link}。', + form: '然后将同步方式选为 GitHub Gist,将你的 token 填入下方出现的输入框中。', + backup: '最后,点击备份按钮即可将本地数据导入到你的 gist 里。' + }, + query: { + title: '如何查询其他浏览器备份的数据?', + p1: '如果您在上述步骤中正确设置了 token,只需简单三步即可查询远端数据。', + enter: '首先,进入管理页{link},点击菜单项【{menuItem}】。', + enable: '如果 token 设置正确,页面右上角会出现一个{icon}图标,点击它即可开启远端查询。', + wait: '等待数据查询完毕,将鼠标移动到数值上,即可查看每个客户端的数据。', + tip: '因为远端数据时按月份分片存放,所以查询时间段不宜过长。', + } + }, + zh_TW: { + title: '使用 GitHub Gist 備份數據', + p1: '這個擴展支持用戶通過簡單的設置,使用 GitHub Gist{link} 備份數據。', + upload: { + title: '簡單四步完成設置', + prepareToken: '首先,您需要在 GitHub 生成一個包含 gist 權限的 token{link}。', + enter: '進入擴展的選項頁面{link}。', + form: '然後將同步方式選為 GitHub Gist,將你的 token 填入下方出現的輸入框中。', + backup: '最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。' + }, + query: { + title: '如何查詢其他瀏覽器備份的數據?', + p1: '如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。', + enter: '首先,進入管理頁{link},點擊菜單項【{menuItem}】。', + enable: '如果 token 設置正確,頁面右上角會出現一個{icon}圖標,點擊它即可開啟遠端查詢。', + wait: '等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。', + tip: '因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。', + } + }, + ja: { + title: 'GitHub Gist でデータをバックアップする', + p1: 'この拡張機能は、簡単な設定で GitHub Gist{link} を使用してデータをバックアップするユーザーをサポートします。', + upload: { + title: 'セットアップを完了するための 4 つの簡単なステップ', + prepareToken: 'まず、GitHub{link} で Gist 権限を持つトークンを生成します。', + enter: 'オプションページ{link}に入ります。', + form: '次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。', + backup: '[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。', + }, + query: { + title: '他のブラウザでバックアップされたデータを照会する方法は?', + p1: '上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + enable: 'トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。', + wait: 'データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。', + tip: 'リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/home.ts b/src/i18n/message/guide/home.ts new file mode 100644 index 000000000..58197aff5 --- /dev/null +++ b/src/i18n/message/guide/home.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _Key = + | 'desc' + | 'button' + | 'download' + +export type HomeMessage = { + [key in _Key]: string +} + +const _default: Messages = { + en: { + desc: 'Timer can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.', + button: 'Start Now!', + download: 'Install for {browser}', + }, + zh_CN: { + desc: '网费很贵是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。', + button: '如何使用', + download: '在 {browser} 上安装', + }, + zh_TW: { + desc: '網費很貴是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。', + button: '如何使用', + download: '在 {browser} 上安裝', + }, + ja: { + desc: 'この拡張機能は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ち、ウェブ習慣を洞察して改善することができます.', + button: 'すぐに始めましょう', + download: '{browser} にインストール', + } +} + +export default _default diff --git a/src/i18n/message/guide/index.ts b/src/i18n/message/guide/index.ts index 399d156aa..d11cb7325 100644 --- a/src/i18n/message/guide/index.ts +++ b/src/i18n/message/guide/index.ts @@ -1,58 +1,100 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import layoutMessages, { LayoutMessage } from "./layout" -import profileMessages, { ProfileMessage } from "./profile" -import usageMessages, { UsageMessage } from "./usage" +import startMessages, { StartMessage } from "./start" import privacyMessages, { PrivacyMessage } from "./privacy" import metaMessages, { MetaMessage } from "../common/meta" import baseMessages, { BaseMessage } from "../common/base" +import homeMessages, { HomeMessage } from "./home" +import appMessages, { AppMessage } from "./app" +import mergeMessages, { MergeMessage } from "./merge" +import virtualMessages, { VirtualMessage } from "./virtual" +import limitMessages, { LimitMessage } from "./limit" +import backupMessages, { BackupMessage } from "./backup" +import mergeCommonMessages, { MergeCommonMessage } from "../common/merge" +import appMenuMessages, { MenuMessage as AppMenuMessage } from "../app/menu" export type GuideMessage = { + mergeCommon: MergeCommonMessage layout: LayoutMessage - profile: ProfileMessage - usage: UsageMessage + home: HomeMessage + start: StartMessage privacy: PrivacyMessage meta: MetaMessage base: BaseMessage + app: AppMessage + merge: MergeMessage + virtual: VirtualMessage + limit: LimitMessage + backup: BackupMessage + appMenu: AppMenuMessage } const _default: Messages = { zh_CN: { + mergeCommon: mergeCommonMessages.zh_CN, layout: layoutMessages.zh_CN, - profile: profileMessages.zh_CN, - usage: usageMessages.zh_CN, + home: homeMessages.zh_CN, + start: startMessages.zh_CN, privacy: privacyMessages.zh_CN, meta: metaMessages.zh_CN, base: baseMessages.zh_CN, + app: appMessages.zh_CN, + merge: mergeMessages.zh_CN, + virtual: virtualMessages.zh_CN, + limit: limitMessages.zh_CN, + backup: backupMessages.zh_CN, + appMenu: appMenuMessages.zh_CN, }, zh_TW: { + mergeCommon: mergeCommonMessages.zh_TW, layout: layoutMessages.zh_TW, - profile: profileMessages.zh_TW, - usage: usageMessages.zh_TW, + home: homeMessages.zh_TW, + start: startMessages.zh_TW, privacy: privacyMessages.zh_TW, meta: metaMessages.zh_TW, base: baseMessages.zh_TW, + app: appMessages.zh_TW, + merge: mergeMessages.zh_TW, + virtual: virtualMessages.zh_TW, + limit: limitMessages.zh_TW, + backup: backupMessages.zh_TW, + appMenu: appMenuMessages.zh_TW, }, en: { + mergeCommon: mergeCommonMessages.en, layout: layoutMessages.en, - profile: profileMessages.en, - usage: usageMessages.en, + home: homeMessages.en, + start: startMessages.en, privacy: privacyMessages.en, meta: metaMessages.en, base: baseMessages.en, + app: appMessages.en, + merge: mergeMessages.en, + virtual: virtualMessages.en, + limit: limitMessages.en, + backup: backupMessages.en, + appMenu: appMenuMessages.en, }, ja: { + mergeCommon: mergeCommonMessages.ja, layout: layoutMessages.ja, - profile: profileMessages.ja, - usage: usageMessages.ja, + home: homeMessages.ja, + start: startMessages.ja, privacy: privacyMessages.ja, meta: metaMessages.ja, base: baseMessages.ja, + app: appMessages.ja, + merge: mergeMessages.ja, + virtual: virtualMessages.ja, + limit: limitMessages.ja, + backup: backupMessages.ja, + appMenu: appMenuMessages.ja, }, } diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 4be2cc2ac..26cd489f1 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -1,95 +1,55 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ export type LayoutMessage = { + header: { + sourceCode: string + email: string + } menu: { - profile: string - usage: { - title: string - quickstart: string - background: string - advanced: string - backup: string - } - privacy: { - title: string - scope: string - storage: string - } + usage: string } } const _default: Messages = { zh_CN: { + header: { + sourceCode: '查看源代码', + email: '联系作者', + }, menu: { - profile: '欢迎安装{appName}', - usage: { - title: '如何使用', - quickstart: '快速开始', - background: '访问后台页面', - advanced: '高级功能', - backup: '使用 Gist 备份数据', - }, - privacy: { - title: '隐私声明', - scope: '收集哪些数据', - storage: '如何处理这些数据', - }, + usage: '高级用法' }, }, zh_TW: { + header: { + sourceCode: '查看源代碼', + email: '聯繫作者', + }, menu: { - profile: '歡迎安裝{appName}', - usage: { - title: '如何使用', - quickstart: '快速開始', - background: '訪問後台頁面', - advanced: '高級功能', - backup: '使用 Gist 備份數據', - }, - privacy: { - title: '隱私聲明', - scope: '收集哪些數據', - storage: '如何處理這些數據', - }, + usage: '高級用法', }, }, en: { + header: { + sourceCode: 'View source code', + email: 'Contact author' + }, menu: { - profile: 'Welcome to install {appName}', - usage: { - title: 'Using Timer', - quickstart: 'Quickstart', - background: 'Using all functions', - advanced: 'Advanced features', - backup: 'Backup your data with Gist', - }, - privacy: { - title: 'Privary Policy', - scope: 'Personal data collected', - storage: 'How to do with this data', - }, + usage: 'Advanced usages', }, }, ja: { + header: { + sourceCode: 'ソースコードを見る', + email: '著者に連絡する', + }, menu: { - profile: '{appName}へようこそ', - usage: { - title: '使い方', - quickstart: 'クイックスタート', - background: 'すべての機能', - advanced: '高度な機能', - backup: 'Gist でデータをバックアップ', - }, - privacy: { - title: 'ポリシーと規約', - scope: '収集する情報', - storage: 'このデータをどうするか', - }, + usage: '高度な使い方', }, }, } diff --git a/src/i18n/message/guide/limit.ts b/src/i18n/message/guide/limit.ts new file mode 100644 index 000000000..a9bf84895 --- /dev/null +++ b/src/i18n/message/guide/limit.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type LimitMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + check: string + } +} + +const _default: Messages = { + en: { + title: 'Limit browsing time of everyday', + p1: 'If you want to limit the time of browsing certain URLs each day, you can do so by creating a daily time limit rule.', + step: { + title: 'Four steps to create a limit rule', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + click: 'Click the New button in the upper right corner.', + form: 'Paste the URL that needs to be restricted, and the duration of the restriction. ' + + 'Some URL fragments can be replaced with wildcards as needed, or deleted directly. Then click Save.', + check: 'Finally, check whether the target URL hits the newly added rule through the Test button in the upper right corner.', + } + }, + zh_CN: { + title: '限制每天的浏览时间', + p1: '如果你想限制每天浏览某些 URL 的时长,可以通过创建每日时限规则来完成。', + step: { + title: '简单四步创建一个限制规则', + enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', + click: '然后单击右上角的新建按钮。', + form: '粘贴需要限制的 URL,以及限制时长。可以根据需要将部分 URL 片段使用通配符代替,或者直接删除。然后点击保存。', + check: '最后通过右上角的测试功能检查目标 URL 是否命中了刚添加的规则。', + } + }, + zh_TW: { + title: '限制每天的瀏覽時間', + p1: '如果你想限制每天瀏覽某些 URL 的時長,可以通過創建每日時限規則來完成。', + step: { + title: '簡單四步創建一個限制規則', + enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', + click: '然後單擊右上角的新建按鈕。', + form: '粘貼需要限制的 URL,以及限制時長。可以根據需要將部分 URL 片段使用通配符代替,或者直接刪除。然後點擊保存。', + check: '最後通過右上角的測試功能檢查目標 URL 是否命中了剛添加的規則。', + } + }, + ja: { + title: '毎日の閲覧時間を制限する', + p1: '特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。', + step: { + title: '制限ルールを作成するための 4 つのステップ', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + click: '右上隅にある [新規] ボタンをクリックします。', + form: '制限する必要がある URL と制限の期間を貼り付けます。' + + '一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。', + check: '最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/merge.ts b/src/i18n/message/guide/merge.ts new file mode 100644 index 000000000..a6168ee12 --- /dev/null +++ b/src/i18n/message/guide/merge.ts @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type MergeMessage = { + title: string + p1: string + p2: string + lookTitle: string + p3: string + sourceCol: string + targetCol: string + remarkCol: string + source: { + title: string + p1: string + exampleCol: string + only: string + } + target: { + title: string + p1: string + lookCol: string + remark: { + blank: string + spec: string + integer: string + specFirst: string + miss: string + } + p2: string + originalCol: string + mergedCol: string + hitCol: string + } +} + +const _default: Messages = { + en: { + title: 'Summarize data of related sites', + p1: 'This extension is counted by domain name, for example, {demo1} and {demo2} will be counted as 2 records. ' + + 'If you want to see aggregated data for both sites, you\'ll need to use the merge feature.', + p2: 'On most data display pages, merged queries are supported. And users can customize the merge rules on the background page{link}.', + lookTitle: 'What\'s rules look like?', + p3: 'The rule consists of two parts, the source part and the target part. ' + + 'The source part declares which sites hit the rule, and the target part defines how those sites are merged. ' + + 'For example, {demo} is a common rule, the left is the source site, and the right is the target site.', + sourceCol: 'Source part', + targetCol: 'Target part', + remarkCol: 'Remark', + source: { + title: 'How to define the source part?', + p1: 'The source part can be a specific domain name or an Ant expression. Below are some examples.', + exampleCol: 'Examples of matched site', + only: "Only {source} can hit this rule" + }, + target: { + title: 'How to define the target part?', + p1: 'The target part can be a specific domain name, a positive integer, or be left blank. ' + + 'They will be introduced one by one in conjunction with the source part in the table below.', + + lookCol: 'Look', + remark: { + blank: '{source} won\'t be merged cause of blank target part', + spec: 'Sites hitting {source} will be merged into the specific {target}', + integer: 'Sites hitting {source} will be merged into the last {target} level domain names', + specFirst: 'When multiple rules are hit, the source part takes precedence for a specific domain name', + miss: 'Merge to the before level of Public Suffix List{link} when no rules are hit', + }, + p2: 'The following table is some merging examples after the above rules are set at the same time.', + originalCol: 'Original site', + mergedCol: 'Merged site', + hitCol: 'Hitted rule', + } + }, + zh_CN: { + title: '合并统计相关站点', + p1: '这个扩展是按照域名进行统计的,比方说 {demo1} 和 {demo2} 会被统计到 2 个条目。' + + '如果您想要查看这两个网站的数据汇总的话,就需要使用合并功能。', + p2: '在大多数的数据展示界面,都支持站点合并查询。并且用户可以在后台页自定义合并规则{link}。', + lookTitle: '规则长什么样?', + p3: '规则由两部分组成,源和目标。源部分声明哪些站点会命中该规则,而目标部分定义如何合并这些站点。' + + '比如 {demo} 就是一个常见的规则,左边的是源,右边的是目标。', + source: { + title: '如何定义源部分?', + p1: '源部分可以是具体的域名,也可以是 Ant 表达式。下面是一些例子。', + exampleCol: '匹配网站的示例', + only: '只有 {source} 能够命中该规则', + }, + sourceCol: '源部分', + targetCol: '目标部分', + remarkCol: '备注', + target: { + title: '如何定义目标部分?', + p1: '目标部分可以是具体的域名,正整数,或者留空。将在下表中结合源部分一一介绍。', + lookCol: '规则外观', + remark: { + blank: '{source} 不会被合并,因为目标部分为空', + spec: '满足 {source} 的网站会被合并到指定条目 {target}', + integer: '满足 {source} 的网站,在合并时会保留后 {target} 级域名', + specFirst: '命中多个规则时,源部分是具体域名的优先', + miss: '没有命中任何规则时,合并至 Public Suffix List{link} 的前一级', + }, + p2: '下表是上述规则同时设置后的一些合并示例。', + originalCol: '原始站点', + mergedCol: '合并后站点', + hitCol: '命中的规则', + }, + }, + zh_TW: { + title: '合併統計相關站點', + p1: '這個擴展是按照域名進行統計的,比方說 {demo1} 和 {demo2} 會被統計到 2 個條目。如果您想要查看這兩個網站的數據匯總的話,就需要使用合併功能。', + p2: '在大多數的數據展示界面,都支持站點合併查詢。並且用戶可以在後台頁自定義合併規則{link}。', + lookTitle: '規則長什麼樣?', + p3: '規則由兩部分組成,源和目標。源部分聲明哪些站點會命中該規則,而目標部分定義如何合併這些站點。比如 {demo} 就是一個常見的規則,左邊的是源,右邊的是目標。', + source: { + title: '如何定義源部分?', + p1: '源部分可以是具體的域名,也可以是 Ant 表達式。下面是一些例子。', + exampleCol: '匹配網站的示例', + only: '只有 {source} 能够命中该规则', + }, + sourceCol: '源部分', + targetCol: '目標部分', + remarkCol: '備註', + target: { + title: '如何定義目標部分?', + p1: '目標部分可以是具體的域名,正整數,或者留空。將在下表中結合源部分一一介紹。', + lookCol: '規則外觀', + remark: { + blank: '{source} 不会被合并,因为目标部分为空', + spec: '滿足 {source} 的網站會被合併到指定條目 {target}', + integer: '滿足 {source} 的網站,在合併時會保留後 {target} 級域名', + specFirst: '命中多個規則時,源部分是具體域名的優先', + miss: '沒有命中任何規則時,合併至 Public Suffix List{link} 的前一級', + }, + p2: '下表是上述規則同時設置後的一些合併示例。', + originalCol: '原始站點', + mergedCol: '合併後站點', + hitCol: '命中的規則', + }, + }, + ja: { + title: '関連サイトのデータをまとめます', + p1: 'この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。' + + '両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。', + p2: 'ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ{link}でマージ ルールをカスタマイズできます。', + lookTitle: 'ルールはどのように見えますか?', + p3: 'ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。' + + 'ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。' + + 'たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。', + sourceCol: 'ソース部分', + targetCol: '対象部位', + remarkCol: '述べる', + source: { + title: 'ソース パーツを定義する方法', + p1: 'ソース部分は、特定のドメイン名または Ant 式にすることができます。 以下にいくつかの例を示します。', + exampleCol: 'マッチしたサイトの例', + only: "このルールに該当するのは {source} だけです" + }, + target: { + title: 'ターゲット パーツを定義する方法', + p1: 'ターゲット部分は、特定のドメイン名、正の整数、または空白のままにすることができます。' + + '下表のソース部分と合わせて順次紹介していきます。', + + lookCol: '外観', + remark: { + blank: '{source} はマージされません 空白のターゲット パーツが原因です', + spec: '{source} にヒットしたサイトは、特定の {target} にマージされます', + integer: '{source} にヒットしたサイトは、最後の {target} レベルのドメイン名にマージされます', + specFirst: '複数のルールがヒットした場合、特定のドメイン名についてソース部分が優先されます', + miss: 'ルールにヒットしない場合は、Public Suffix List{link} の前のレベルにマージします', + }, + p2: '次の表は、上記のルールを同時に設定した後のいくつかのマージ例です。', + originalCol: '元のサイト', + mergedCol: '統合サイト', + hitCol: 'ヒットルール', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/privacy.ts b/src/i18n/message/guide/privacy.ts index 7e8401c28..be36a95d7 100644 --- a/src/i18n/message/guide/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -1,75 +1,181 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ - type _StoreKey = + | 'title' | 'p1' | 'p2' | 'p3' -type _ScopeKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' + +type RequiredScope = { + name: string + usage: string +} + +type OptionalScope = RequiredScope & { + optionalReason: string +} + +export type Scope = RequiredScope & { + optionalReason?: string +} export type PrivacyMessage = { - scope: { [key in _ScopeKey]: string } + title: string + alert: string + scope: { + title: string + cols: { + name: string + usage: string + required: string + } + rows: { + website: RequiredScope + tab: OptionalScope + clipboard: OptionalScope + } + } storage: { [key in _StoreKey]: string } } const _default: Messages = { zh_CN: { + title: '隐私声明', + alert: '为了向您提供完整的服务,该扩展在使用过程中会必要地收集您的一些个人数据,详情见以下隐私声明。', scope: { - p1: '为了向您提供完整的服务,该扩展在使用过程中会收集您以下的个人信息:', - l1: '1. 您浏览网站的时间,以及访问每个网站的次数。', - l2: '2. 网站的标题以及图标 URL。', - l3: '3. 为了提高用户体验,扩展在必要时会征得您的授权之后,读取您的剪切板内容。', + title: '哪些数据会被收集?', + cols: { + name: '内容', + usage: '用途', + required: '是否必需', + }, + rows: { + website: { + name: '网站访问记录', + usage: '用于统计浏览时长和访问次数', + }, + tab: { + name: '浏览器标签信息', + usage: '用于自动获取网站的名称和图标,展示数据时提升用户体验', + optionalReason: '只有在选项里开启自动获取功能后才会收集', + }, + clipboard: { + name: '剪切板内容', + usage: '在设置每日时限规则时,为了操作方便,会读取剪切板内 URL', + optionalReason: '需要用户手动同意' + }, + }, }, storage: { + title: '如何处理这些数据?', p1: '我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。', p2: '不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。', p3: '我们只帮助您收集数据,但处置权一定在您。', }, }, zh_TW: { + title: '隱私聲明', + alert: '為了向您提供完整的服務,該擴展在使用過程中會必要地收集您的一些個人數據,詳情見以下隱私聲明。', scope: { - p1: '為了向您提供完整的服務,該擴展在使用過程中會收集您以下的個人信息:', - l1: '1. 您瀏覽網站的時間,以及訪問每個網站的次數。', - l2: '2. 網站的標題以及圖標 URL。', - l3: '3. 為了提高用戶體驗,擴展在必要時會徵得您的授權之後,讀取您的剪切板內容。', + title: '哪些數據會被收集?', + cols: { + name: '內容', + usage: '用途', + required: '是否必需', + }, + rows: { + website: { + name: '網站訪問記錄', + usage: '用於統計瀏覽時長和訪問次數', + }, + tab: { + name: '瀏覽器標籤信息', + usage: '用於自動獲取網站的名稱和圖標,展示數據時提升用戶體驗', + optionalReason: '只有在選項裡開啟自動獲取功能後才會收集', + }, + clipboard: { + name: '剪切板內容', + usage: '在設置每日時限規則時,為了操作方便,會讀取剪切板內 URL', + optionalReason: '需要用戶手動同意' + }, + }, }, storage: { + title: '如何處理這些數據?', p1: '我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。', p2: '不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。', p3: '我們只幫助您收集數據,但處置權一定在您。', }, }, en: { + title: 'Privacy statement', + alert: 'In order to provide you with complete services, this extension will necessarily collect some of your personal data during use, see the following privacy statement for details.', scope: { - p1: 'In order to provide you with complete services, this extension will collect your following personal information during use:', - l1: '1. How long you browse the site, and how many times you visit each site.', - l2: '2. The title of the website and the URL of the icon.', - l3: '3. In order to improve user experience, the extension will read your clipboard content after obtaining your authorization when necessary.', + title: 'What data is collected?', + cols: { + name: 'Content', + usage: 'Usage', + required: 'Required', + }, + rows: { + website: { + name: 'Website browsing history', + usage: 'Used to count browsing time and visits', + }, + tab: { + name: 'Tab information', + usage: 'Used to automatically obtain the name and icon of the website, and improve user experience when displaying data', + optionalReason: 'Only if this function is enabled in the options', + }, + clipboard: { + name: 'Clipboard content', + usage: 'When setting the daily time limit rule, for the convenience of operation, the URL in the clipboard will be read', + optionalReason: 'Only if user agreed' + }, + }, }, storage: { + title: 'How to do with this data?', 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.', p3: 'We only help you collect data, but the right of disposal must be yours.', }, }, ja: { + title: 'プライバシーに関する声明', + alert: '完全なサービスを提供するために、この拡張機能は使用中に必ず個人データの一部を収集します。詳細については、次のプライバシーに関する声明を参照してください。', scope: { - p1: '完全なサービスを提供するために、この拡張機能は使用中に次の個人情報を収集します。', - l1: '1. サイトを閲覧した時間と、各サイトにアクセスした回数。', - l2: '2. ウェブサイトのタイトルとアイコンの URL。', - l3: '3. ユーザー エクスペリエンスを向上させるために、拡張機能は必要に応じて承認を得た後、クリップボードの内容を読み取ります。', + title: 'どのようなデータが収集されますか?', + cols: { + name: '収集データ', + usage: '使用', + required: 'それは必要ですか', + }, + rows: { + website: { + name: 'ウェブサイトの閲覧履歴', + usage: '閲覧時間と訪問をカウントするために使用されます', + }, + tab: { + name: 'タブ情報', + usage: 'Web サイトの名前とアイコンを自動的に取得し、データを表示する際のユーザー エクスペリエンスを向上させるために使用されます', + optionalReason: 'この機能がオプションで有効になっている場合のみ', + }, + clipboard: { + name: 'クリップボードの内容', + usage: '毎日の時間制限ルールを設定すると、操作の便宜上、クリップボードの URL が読み込まれます', + optionalReason: 'ユーザーが同意した場合のみ' + }, + }, }, storage: { + title: 'このデータをどうするか?', p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', diff --git a/src/i18n/message/guide/profile.ts b/src/i18n/message/guide/profile.ts deleted file mode 100644 index afa6c876a..000000000 --- a/src/i18n/message/guide/profile.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _key = - | 'p1' - | 'p2' - -export type ProfileMessage = { - [key in _key]: string -} - -const _default: Messages = { - zh_CN: { - p1: '{appName}是一款开源、免费、用户友好的,用于统计上网时间的浏览器扩展。您可以在 {github} 上查阅它的源代码。', - p2: '这个页面将会告诉您如何使用它,以及相关的隐私政策。', - }, - zh_TW: { - p1: '{appName}是一款開源、免費、用戶友好的,用於統計上網時間的瀏覽器擴展。您可以在 {github} 上查閱它的源代碼。', - 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}.', - p2: 'This page will tell you how to use it, and the related privacy policy.', - }, - ja: { - p1: '{appName}は、オンラインで費やした時間をカウントするための、オープン ソースで無料のユーザー フレンドリーなブラウザ拡張機能です。 {github} でソース コードを確認できます。 ', - p2: 'このページでは、使用方法と関連するプライバシー ポリシーについて説明します。', - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/start.ts b/src/i18n/message/guide/start.ts new file mode 100644 index 000000000..1c180f2a1 --- /dev/null +++ b/src/i18n/message/guide/start.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _Key = + | 'p1' + | 's1' + | 's1p1' + | 's2' + | 's2p1' + | 's3' + | 's3p1' + | 'alert' + +export type StartMessage = { + title: string +} & { + [key in _Key]: string + } + +const _default: Messages = { + zh_CN: { + title: '快速开始', + p1: '只需简单三步,就可以快速开始使用这个扩展。', + s1: '1. 固定扩展的图标', + s1p1: '首先,为了更方便地使用这个扩展,你需要将图标固定到工具栏上。不同浏览器的操作方式不同,下图显示了在 Chrome 中的做法。', + s2: '2. 浏览任何网站', + s2p1: '然后你可以点开任何网站,扩展图标上会展示该网站的当日浏览时间,就像这样 {demo}。', + s3: '3. 在弹出页中查看数据', + s3p1: '最后,点击扩展的图标,你可以在弹出的页面里看到今天、本周以及本月的饼状图数据。', + alert: '你已经学会了基本用法,赶紧试试!!', + }, + en: { + title: 'Get started', + p1: 'You can quickly start using this extension in just 3 easy steps.', + s1: '1. Pin the icon', + s1p1: 'Firstly, to use this extension more conveniently, you\'d better pin the icon to toolbar. It\'s not the same in different browsers to do this, the following figure shows how in Chrome.', + s2: '2. Browse any website', + s2p1: 'Then, browse any website, and you will see that the time is beating on the icon, just like this {demo}.', + s3: '3. Read data in the popup page', + s3p1: 'Finally, click the icon to open the popup page, and you can read the data visualized with pie chart, of today, this week or this month.', + alert: 'You have learned the basic usage, try it!!', + }, + zh_TW: { + title: '快速開始', + p1: '只需簡單三步,就可以快速開始使用這個擴展。', + s1: '1. 固定擴展的圖標', + s1p1: '首先,為了更方便地使用這個擴展,你需要將圖標固定到工具欄上。不同瀏覽器的操作方式不同,下圖顯示了在 Chrome 中的做法。', + s2: '2. 瀏覽任何網站', + s2p1: '然後你可以點開任何網站,擴展圖標上會展示該網站的當日瀏覽時間,就像這樣 {demo}。', + s3: '3. 在彈出頁中查看數據', + s3p1: '最後,點擊擴展的圖標,你可以在彈出的頁面裡看到今天、本週以及本月的餅狀圖數據。', + alert: '你已經學會了基本用法,趕緊試試!!', + }, + ja: { + title: '始めましょう', + p1: 'この拡張機能は、わずか 3 つの簡単な手順ですぐに使い始めることができます。', + s1: '1. アイコンを固定する', + s1p1: 'まず、この拡張機能をより便利に使用するには、アイコンをツールバーにピン留めすることをお勧めします。 これを行う方法はブラウザーによって異なります。次の図は、Chrome での方法を示しています。', + s2: '2. 任意の Web サイトを閲覧する', + s2p1: '次に、任意の Web サイトを閲覧すると、この {demo} のように、時間がアイコンに刻み込まれていることがわかります。', + s3: '3. ポップアップ ページでデータを読み取る', + s3p1: '最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。', + alert: '基本的な使い方を学んだので、試してみてください!!', + }, +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/usage.ts b/src/i18n/message/guide/usage.ts deleted file mode 100644 index 2026a441b..000000000 --- a/src/i18n/message/guide/usage.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _QuickstartKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'p2' - -type _BackgroundKey = - | 'p1' - | 'l1' - | 'l2' - | 'p2' - | 'backgroundPage' - -type _AdvancedKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'l4' - | 'l5' - | 'l6' - | 'l7' - | 'l8' - -type _BackupKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - -export type UsageMessage = { - quickstart: { [key in _QuickstartKey]: string } - background: { [key in _BackgroundKey]: string } - advanced: { [key in _AdvancedKey]: string } - backup: { [key in _BackupKey]: string } -} - -const _default: Messages = { - zh_CN: { - quickstart: { - p1: '首先,您可以通过以下几步,开始体验这个扩展:', - l1: '1. 将扩展的图标固定在浏览器的右上角,具体的操作方法根据您的浏览器而定。该步骤不会影响扩展的正常运行,但是将极大改善您的交互体验。', - l2: '2. 打开任意一个网站,浏览几秒钟,这时您会观察到右上角的图标上有数字跳动,它显示您今天花了多少时间浏览当前网站。', - l3: '3. 点击扩展的图标,会弹出一个页面,展示今天或最近一段时间您的上网数据。', - p2: '需要提醒的是,由于时长数据是实时统计,所以安装扩展之前的浏览记录不会被记录。', - }, - background: { - p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的 {background}。进入后台页有以下两种方式:', - l1: '1. 您可以右击扩展的图标,在弹出的菜单中点击【{allFunction}】。', - l2: '2. 您在图标弹出页的下方也可以找到【{allFunction}】链接,同样点击它即可。', - p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。', - backgroundPage: '后台页', - }, - advanced: { - p1: '这个扩展的核心功能是统计您在不同网站上的浏览行为。此外,它也提供了很多高级功能,来满足您更多的需求。当然,所有的功能您都可以在后台页里找到。', - l1: '1. 它可以分析您在一段时间内访问同一个网站的趋势,并以折线图展示。', - l2: '2. 它可以统计您在每天的不同时间段里的上网频率,并以直方图展示。该数据不区分网站,最小的统计粒度为 15 分钟。', - l3: '3. 它可以统计您阅读本地文件的时间,不过该功能需要在选项中启用。', - l4: '4. 它支持白名单功能,您可以将您不想要统计的网站加入白名单。', - l5: '5. 它支持将几个相关的网站合并统计到同一个条目,您可以自定义合并的规则。默认按照 {psl} 合并。', - l6: '6. 它支持限制每个网站的每日浏览时长,需要您手动添加限制规则。', - l7: '7. 它支持夜间模式,同样需要在选项里启用。', - l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。', - }, - backup: { - p1: '您可以按以下步骤使用 {gist} 备份您的数据。之后,您可在其他终端上查询已备份数据。', - l1: '1. 首先,您需要在 Github 生成一个包含 gist 权限的 {token}。', - l2: '2. 然后在选项页面将同步方式选为 Github Gist,将你的 token 填入下方出现的输入框中。', - l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。', - }, - }, - zh_TW: { - quickstart: { - p1: '首先,您可以通過以下幾步,開始體驗這個擴展:', - l1: '1. 將擴展的圖標固定在瀏覽器的右上角,具體的操作方法根據您的瀏覽器而定。該步驟不會影響擴展的正常運行,但是將極大改善您的交互體驗。', - l2: '2. 打開任意一個網站,瀏覽幾秒鐘,這時您會觀察到右上角的圖標上有數字跳動,它顯示您今天花了多少時間瀏覽當前網站。', - l3: '3. 點擊擴展的圖標,會彈出一個頁面,展示今天或最近一段時間您的上網數據。', - p2: '需要提醒的是,由於時長數據是實時統計,所以安裝擴展之前的瀏覽記錄不會被記錄。', - }, - background: { - p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的 {background}。進入後台頁有以下兩種方式:', - l1: '1. 您可以右擊擴展的圖標,在彈出的菜單中點擊【{allFunction}】。', - l2: '2. 您在圖標彈出頁的下方也可以找到【{allFunction}】鏈接,同樣點擊它即可。', - p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', - backgroundPage: '後台頁', - }, - advanced: { - p1: '這個擴展的核心功能是統計您在不同網站上的瀏覽行為。此外,它也提供了很多高級功能,來滿足您更多的需求。當然,所有的功能您都可以在後台頁裡找到。', - l1: '1. 它可以分析您在一段時間內訪問同一個網站的趨勢,並以折線圖展示。', - l2: '2. 它可以統計您在每天的不同時間段裡的上網頻率,並以直方圖展示。該數據不區分網站,最小的統計粒度為 15 分鐘。', - l3: '3. 它可以統計您閱讀本地文件的時間,不過該功能需要在選項中啟用。', - l4: '4. 它支持白名單功能,您可以將您不想要統計的網站加入白名單。', - l5: '5. 它支持將幾個相關的網站合併統計到同一個條目,您可以自定義合併的規則。默認按照 {psl} 合併。', - l6: '6. 它支持限制每個網站的每日瀏覽時長,需要您手動添加限制規則。', - l7: '7. 它支持夜間模式,同樣需要在選項裡啟用。', - l8: '8. 它支持使用 Github Gist 作為雲端存儲多個瀏覽器的數據,並進行聚合查詢。需要您準備一個至少包含 gist 權限的 token。', - }, - backup: { - p1: '您可以按以下步驟使用 {gist} 備份您的數據。之後,您可在其他終端上查詢已備份數據。', - l1: '1. 首先,您需要在 Github 生成一個包含 gist 權限的 {token}。', - l2: '2. 然後在選項頁面將同步方式選為 Github Gist,將你的 token 填入下方出現的輸入框中。', - 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', - 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.', - }, - 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:', - 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.', - 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.', - 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.', - l6: '6. It supports limiting the daily browsing time of each website, which requires you to manually add limiting rules.', - l7: '7. It supports night mode, which also needs to be enabled in the options.', - 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.', - 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.', - }, - }, - ja: { - quickstart: { - p1: 'まず、次の手順に従って、この拡張機能の調査を開始できます。', - l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', - l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', - l3: '3. 拡張機能のアイコンをクリックすると、ページがポップアップし、今日または最近のインターネット データが表示されます。', - p2: 'なお、継続時間データはリアルタイムでカウントされるため、拡張機能をインストールする前の閲覧履歴は記録されません。', - }, - background: { - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', - l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [{allFunction}] をクリックします。', - l2: '2. また、アイコン ポップアップ ページの下部に [{allFunction}] リンクがあり、それをクリックするだけです。', - p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', - backgroundPage: '背景ページ', - }, - advanced: { - p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', - l1: '1. 一定期間の同じ Web サイトへのアクセスの傾向を分析し、折れ線グラフで表示できます。', - l2: '2. あなたのネットサーフィン頻度を毎日異なる時間帯でカウントし、ヒストグラムで表示できます。 データはサイトにとらわれず、最小の統計粒度は 15 分です。', - l3: '3. ローカル ファイルの読み取り時間をカウントできますが、この機能はオプションで有効にする必要があります。', - l4: '4. ホワイトリスト機能をサポートしており、カウントしたくない Web サイトをホワイトリストに追加できます。', - l5: '5. 複数の関連 Web サイトの統計を同じエントリにマージすることをサポートし、マージのルールをカスタマイズできます。 デフォルトでは {psl} でマージします。', - l6: '6. 各 Web サイトの毎日の閲覧時間の制限をサポートしています。これには、制限ルールを手動で追加する必要があります。', - l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', - l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', - }, - backup: { - p1: '以下の手順に従って、{gist} を使用してデータをバックアップできます。その後、バックアップされたデータを他の端末で照会できます。', - l1: '1. まず、Github で Gist 権限を持つ {token} を生成する必要があります。', - l2: '2. 次に、オプション ページで同期方法として [Github Gist] を選択し、下に表示される入力ボックスにトークンを入力します。', - l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。', - }, - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/virtual.ts b/src/i18n/message/guide/virtual.ts new file mode 100644 index 000000000..30eaa5945 --- /dev/null +++ b/src/i18n/message/guide/virtual.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type VirtualMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + browse: string + } +} + +const _default: Messages = { + en: { + title: 'Count data for specific URLs', + p1: 'If you want to count the browsing time and number of visits of certain URLs, not just the domain name, ' + + 'you can do so by creating a virtual site.', + step: { + title: 'Four steps to create a virtual site', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + click: 'Click the New button in the upper right corner.', + form: 'Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, ' + + 'and then click Save.', + browse: 'Finally, Browse the relevant URLs and observe the data.', + } + }, + zh_CN: { + title: '统计特定 URL 的数据', + p1: '如果你想统计某些 URL ,而不只是域名的浏览时长和访问次数,可以通过创建虚拟站点来实现。', + step: { + title: '简单四步创建一个虚拟站点', + enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', + click: '然后单击右上角的新建按钮。', + form: '填写要统计的 URL,可以使用 Ant 表达式,如 {demo1} 或 {demo2} ,以及这个虚拟站点的名称,然后点击保存。', + browse: '最后浏览相关的网页,观察数据。' + } + }, + zh_TW: { + title: '統計特定 URL 的數據', + p1: '如果你想統計某些 URL ,而不只是域名的瀏覽時長和訪問次數,可以通過創建虛擬站點來實現。', + step: { + title: '簡單四步創建一個虛擬站點', + enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', + click: '然後單擊右上角的新建按鈕。', + form: '填寫要統計的 URL,可以使用 Ant 表達式,如 {demo1} 或 {demo2} ,以及這個虛擬站點的名稱,然後點擊保存。', + browse: '最後瀏覽相關的網頁,觀察數據。', + }, + }, + ja: { + title: '特定の URL のデータをカウントする', + p1: 'ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。', + step: { + title: '仮想サイトを作成するための 4 つのステップ', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + click: '右上隅にある [新規] ボタンをクリックします。', + form: '次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。', + browse: '最後に、関連する URL を参照してデータを観察します。', + } + }, +} + +export default _default diff --git a/src/manifest.ts b/src/manifest.ts index 5a47abfd0..d9591bab9 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -14,7 +14,7 @@ // Not use path alias in manifest.json import packageInfo from "./package" import { OPTION_ROUTE } from "./app/router/constants" -const { version, author, homepage } = packageInfo +const { version, author: { name: author }, homepage } = packageInfo const _default: chrome.runtime.ManifestV2 = { name: '__MSG_meta_marketName__', description: "__MSG_meta_description__", diff --git a/src/package.ts b/src/package.ts index c442574d7..20fcd31c5 100644 --- a/src/package.ts +++ b/src/package.ts @@ -19,4 +19,9 @@ const _default: _PackageInfo = { author: packageJson.author } +/** + * @since 1.8.0 + */ +export const AUTHOR_EMAIL: string = packageJson.author.email + export default _default \ No newline at end of file diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index 70e7ad199..d4a3d30a3 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -37,9 +37,12 @@ type RegRuleItem = { result: string | number } -const processRegStr = (regStr: string) => regStr.split('.').join('\\.').split('*').join('[^\\.]+') +const processRegStr = (regStr: string) => regStr + .split('.').join('\\.') + .split('**').join('.+') + .split('*').join('[^\\.]+') -const convert = (dbItem: timer.merge.Rule) => { +function convert(dbItem: timer.merge.Rule): RegRuleItem | [string, string | number] { const { origin, merged } = dbItem if (origin.includes('*')) { const regStr = processRegStr(origin) @@ -57,7 +60,9 @@ export default class CustomizedHostMergeRuler implements timer.merge.Merger { constructor(rules: timer.merge.Rule[]) { rules.map(item => convert(item)) - .forEach(rule => Array.isArray(rule) ? (this.noRegMergeRules[rule[0]] = rule[1]) : (this.regulars.push(rule))) + .forEach(rule => Array.isArray(rule) + ? (this.noRegMergeRules[rule[0]] = rule[1] || rule[0]) + : (this.regulars.push(rule))) } /** diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index d4e5aaaac..199626a81 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -90,8 +90,8 @@ export const UPDATE_PAGE = updatePage */ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: any): string { let url = IS_FIREFOX && !isInBackground ? 'app.html' : 'static/app.html' - const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' route && (url += '#' + route) + const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' queries && (url += '?' + queries) return url } @@ -101,8 +101,10 @@ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: a * @param isInBackground invoke in background environment * @since 1.3.2 */ -export function getGuidePageUrl(isInBackground: boolean): string { - return IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' +export function getGuidePageUrl(isInBackground: boolean, route?: string): string { + let url = IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' + route && (url += '#' + route) + return url } /** diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts index 0b0679305..d3615739d 100644 --- a/src/util/dark-mode.ts +++ b/src/util/dark-mode.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -25,7 +25,9 @@ function toggle0(isDarkMode: boolean) { * Init from local storage */ export function init() { - toggle0(isDarkMode()) + const val = isDarkMode() + toggle0(val) + return val } export function toggle(isDarkMode: boolean) { diff --git a/src/util/merge.ts b/src/util/merge.ts new file mode 100644 index 000000000..a7d3b8a17 --- /dev/null +++ b/src/util/merge.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { MergeCommonMessage } from "@i18n/message/common/merge" + +export type MergeTagType = '' | 'info' | 'success' + +export function computeMergeType(mergedVal: number | string): MergeTagType { + return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' +} + +type TypeFinder = (mergeCommon: MergeCommonMessage | EmbeddedPartial) => string + +export function computeMergeTxt( + originVal: string, + mergedVal: number | string, + i18n: (finder: TypeFinder, param?: any) => string +): string { + const mergeTxt = typeof mergedVal === 'number' + ? i18n(msg => msg?.tagResult?.level, { level: mergedVal + 1 }) + : mergedVal === '' ? i18n(msg => msg?.tagResult?.blank) : mergedVal + return `${originVal} >>> ${mergeTxt}` +} From f272a7eb8a117ec1357cfd067182c2f0d2bd0dd1 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 4 May 2023 02:14:08 +0800 Subject: [PATCH 141/168] Fix sidebar bgcolor (#208) --- src/api/chrome/tab.ts | 1 - .../components/common/content-container.ts | 6 +- src/app/layout/index.ts | 19 +++-- src/app/layout/menu.ts | 70 +++++++++---------- src/app/styles/index.sass | 12 +++- 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts index 63a3861a1..da3f4c8cd 100644 --- a/src/api/chrome/tab.ts +++ b/src/api/chrome/tab.ts @@ -36,7 +36,6 @@ export function createTab(param: chrome.tabs.CreateProperties | string): Promise */ export async function createTabAfterCurrent(url: string): Promise { const tab = await getCurrentTab() - console.log(tab) if (!tab) { // Current tab not found return createTab(url) diff --git a/src/app/components/common/content-container.ts b/src/app/components/common/content-container.ts index 1a255a98a..cf2938280 100644 --- a/src/app/components/common/content-container.ts +++ b/src/app/components/common/content-container.ts @@ -1,11 +1,11 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElCard } from "element-plus" +import { ElCard, ElScrollbar } from "element-plus" import ContentCard from "./content-card" import { defineComponent, h, useSlots } from "vue" @@ -19,7 +19,7 @@ const _default = defineComponent(() => { } else { content && children.push(h(ContentCard, () => h(content))) } - return () => h("div", { class: "content-container" }, children) + return () => h(ElScrollbar, () => h("div", { class: "content-container" }, children)) }) export default _default \ No newline at end of file diff --git a/src/app/layout/index.ts b/src/app/layout/index.ts index a9b54a5e4..ba9cc7bf9 100644 --- a/src/app/layout/index.ts +++ b/src/app/layout/index.ts @@ -1,25 +1,22 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElAside, ElContainer } from "element-plus" +import { ElAside, ElContainer, ElScrollbar } from "element-plus" import { defineComponent, h } from "vue" import Menu from "./menu" import VersionTag from "./version-tag" import { RouterView } from "vue-router" -const _default = defineComponent({ - name: "Layout", - render() { - return h(ElContainer, {}, () => [ - h(ElAside, { width: `270px` }, () => h(Menu)), - h(ElContainer, { class: 'app-container' }, () => h(RouterView)), - h(VersionTag) - ]) - } +const _default = defineComponent(() => { + return () => h(ElContainer, {}, () => [ + h(ElAside, () => h(ElScrollbar, () => h(Menu))), + h(ElContainer, { class: 'app-container' }, () => h(RouterView)), + h(VersionTag) + ]) }) export default _default \ No newline at end of file diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 7b4a08436..c97d5f4f3 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -5,22 +5,22 @@ * https://opensource.org/licenses/MIT */ -import type { UnwrapRef } from "vue" +import type { Ref } from "vue" import type ElementIcon from "@src/element-ui/icon" import type { MenuItemRegistered } from "element-plus" -import type { RouteLocationNormalizedLoaded, Router } from "vue-router" +import type { Router } from "vue-router" import type { I18nKey } from "@app/locale" import type { MenuMessage } from "@i18n/message/app/menu" -import { defineComponent, h, onMounted, reactive } from "vue" +import { defineComponent, h, onMounted, ref, watch } from "vue" import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup } from "element-plus" -import { useRoute, useRouter } from "vue-router" +import { useRouter } from "vue-router" import { t } from "@app/locale" import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/constant/url" 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" +import { createTabAfterCurrent } from "@api/chrome/tab" import { ANALYSIS_ROUTE, MERGE_ROUTE } from "@app/router/constants" import { START_ROUTE } from "@guide/router/constants" @@ -37,11 +37,6 @@ type _MenuGroup = { children: _MenuItem[] } -type _RouteProps = { - router: Router - current: RouteLocationNormalizedLoaded -} - /** * Generate menu items after locale initialized */ @@ -126,23 +121,24 @@ function generateMenus(): _MenuGroup[] { }] } -function openMenu(route: string, title: I18nKey, routeProps: UnwrapRef<_RouteProps>) { - const routerVal = routeProps.router - const currentRouteVal = routeProps.current - if (currentRouteVal && currentRouteVal.path !== route) { - routerVal && routerVal.push(route) +function openMenu(route: string, title: I18nKey, router: Router) { + const currentPath = router.currentRoute.value?.path + if (currentPath !== route) { + router?.push(route) document.title = t(title) } } -const openHref = (href: string) => createTab(href) +const openHref = (href: string) => createTabAfterCurrent(href) -function handleClick(menuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { +function handleClick(menuItem: _MenuItem, router: Router, currentActive: Ref) { const { route, title, href } = menuItem if (route) { - openMenu(route, msg => msg.menu[title], routeProps) + openMenu(route, msg => msg.menu[title], router) + currentActive.value = '/data/dashboard'//route } else { openHref(href) + currentActive.value = router.currentRoute?.value?.path } } @@ -153,10 +149,10 @@ const iconStyle: Partial = { lineHeight: '0.83em' } -function renderMenuLeaf(menu: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { +function renderMenuLeaf(menu: _MenuItem, router: Router, currentActive: Ref) { const { route, title, icon, index } = menu const props: { onClick: (item: MenuItemRegistered) => void; index?: string } = { - onClick: (_item) => handleClick(menu, routeProps) + onClick: (_item) => handleClick(menu, router, currentActive) } const realIndex = index || route realIndex && (props.index = realIndex) @@ -166,9 +162,9 @@ function renderMenuLeaf(menu: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { }) } -function renderMenu(menu: _MenuGroup, props: UnwrapRef<_RouteProps>) { +function renderMenu(menu: _MenuGroup, router: Router, currentActive: Ref) { const title = t(msg => msg.menu[menu.title]) - return h(ElMenuItemGroup, { title }, () => menu.children.map(item => renderMenuLeaf(item, props))) + return h(ElMenuItemGroup, { title }, () => menu.children.map(item => renderMenuLeaf(item, router, currentActive))) } async function initTitle(allMenus: _MenuGroup[], router: Router) { @@ -185,22 +181,22 @@ async function initTitle(allMenus: _MenuGroup[], router: Router) { } } -const _default = defineComponent({ - name: "LayoutMenu", - setup() { - const routeProps: UnwrapRef<_RouteProps> = reactive({ - router: useRouter(), - current: useRoute() - }) - - const allMenus = generateMenus() - onMounted(() => initTitle(allMenus, useRouter())) - - return () => h(ElMenu, - { defaultActive: routeProps.current.path }, - () => allMenus.map(menu => renderMenu(menu, routeProps)) - ) +const _default = defineComponent(() => { + const router = useRouter() + const currentActive: Ref = ref() + const syncRouter = () => { + const route = router.currentRoute.value + route && (currentActive.value = route.path) } + + watch(router.currentRoute, syncRouter) + + const allMenus = generateMenus() + onMounted(() => initTitle(allMenus, router)) + + return () => h('div', { class: 'menu-container' }, h(ElMenu, { defaultActive: currentActive.value }, + () => allMenus.map(menu => renderMenu(menu, router, currentActive)) + )) }) export default _default \ No newline at end of file diff --git a/src/app/styles/index.sass b/src/app/styles/index.sass index e5cce78d7..59c1455d4 100644 --- a/src/app/styles/index.sass +++ b/src/app/styles/index.sass @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -33,6 +33,11 @@ a padding-left: 45px !important .el-aside width: 240px + .el-menu + padding-bottom: 10px + .menu-container + min-height: 100vh + background-color: var(--el-menu-bg-color) .app-container width: 100% @@ -41,10 +46,11 @@ a .content-container margin-top: 30px - width: calc(100% - 60px) + margin-bottom: 30px + width: calc(100vw - 300px) height: calc(100% - 30px) padding: 0 30px - overflow-y: auto + overflow: hidden #app height: 100% From fad055d48eaf626cffbdf76f5ae5d64105ed48c6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 4 May 2023 02:32:32 +0800 Subject: [PATCH 142/168] Fix bugs in Firefox --- src/api/chrome/window.ts | 8 ++------ src/background/index.ts | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/api/chrome/window.ts b/src/api/chrome/window.ts index b40fc63da..89aa8c4c7 100644 --- a/src/api/chrome/window.ts +++ b/src/api/chrome/window.ts @@ -28,11 +28,7 @@ export function getFocusedNormalWindow(): Promise { } export function getWindow(id: number): Promise { - return new Promise(resolve => - chrome.windows.get(id) - .then(win => resolve(win)) - .catch(_ => resolve(undefined)) - ) + return new Promise(resolve => chrome.windows.get(id, win => resolve(win))) } type _Handler = (windowId: number) => void @@ -41,5 +37,5 @@ 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/background/index.ts b/src/background/index.ts index bc23afe36..9b454e82a 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -22,7 +22,7 @@ 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 { getWindow, isNoneWindowId, onNormalWindowFocusChanged } from "@api/chrome/window" import { onInstalled } from "@api/chrome/runtime" // Open the log of console @@ -62,14 +62,6 @@ new ActiveTabListener() .register(({ url, tabId }) => badgeTextManager.forceUpdate({ url, tabId })) .listen() -// Listen window focus changed -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 onInstalled(async reason => { if (reason === "install") { @@ -81,4 +73,14 @@ onInstalled(async reason => { }) // Start message dispatcher -messageDispatcher.start() \ No newline at end of file +messageDispatcher.start() + +// Listen window focus changed +onNormalWindowFocusChanged(async windowId => { + if (isNoneWindowId(windowId)) return + const window = await getWindow(windowId) + if (!window || window.type !== 'normal') return + const tabs = await listTabs({ windowId, active: true }) + tabs.filter(tab => !isBrowserUrl(tab?.url)) + .forEach(({ url, id }) => badgeTextManager.forceUpdate({ url, tabId: id })) +}) \ No newline at end of file From bbe790261e6dd4cd5cb2fc0e48114afc39a04c49 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 4 May 2023 02:40:31 +0800 Subject: [PATCH 143/168] v1.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c43177fe7..f71c731af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.7.2", + "version": "1.8.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { From e3b919d89551aed078e607b788e915e9c3e699c6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 9 May 2023 14:05:01 +0800 Subject: [PATCH 144/168] Optimize the style of chart charts (#204) --- src/app/components/analysis/components/summary/summary.sass | 1 + src/app/components/dashboard/row1.ts | 4 ++-- src/app/components/dashboard/style/index.sass | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/components/analysis/components/summary/summary.sass b/src/app/components/analysis/components/summary/summary.sass index 238b3a0b3..db9ea8be1 100644 --- a/src/app/components/analysis/components/summary/summary.sass +++ b/src/app/components/analysis/components/summary/summary.sass @@ -1,6 +1,7 @@ .analysis-summary-container height: 140px + margin-bottom: 0px !important >.el-col:not(:first-child) border-left: 1px var(--el-border-color) var(--el-border-style) .site-container diff --git a/src/app/components/dashboard/row1.ts b/src/app/components/dashboard/row1.ts index 697f5362e..29613a19e 100644 --- a/src/app/components/dashboard/row1.ts +++ b/src/app/components/dashboard/row1.ts @@ -15,8 +15,8 @@ import TopKVisit from './components/top-k-visit' const _default = defineComponent({ name: "DashboardRow1", render: () => h(ElRow, { - gutter: 40, - style: { height: '290px' } + gutter: 20, + style: { height: '300px' } }, () => [ h(DashboardCard, { span: 4 diff --git a/src/app/components/dashboard/style/index.sass b/src/app/components/dashboard/style/index.sass index d7d3340a7..cd0d06851 100644 --- a/src/app/components/dashboard/style/index.sass +++ b/src/app/components/dashboard/style/index.sass @@ -6,11 +6,9 @@ */ .el-row - margin-bottom: 50px + margin-bottom: 15px .el-col height: 100% - &:last-child - margin-bottom: 0 .el-card__body padding: 20px From 6c2a8305567d782f9e8778d33295065bea8a8266 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 12 May 2023 00:21:06 +0800 Subject: [PATCH 145/168] Split chunks --- package.json | 1 + public/app.html | 2 - public/guide.html | 18 ++--- public/popup.html | 2 - src/guide/layout/header/header.sass | 2 +- src/guide/style/light-theme.sass | 2 +- src/i18n/chrome/message.ts | 2 +- webpack/webpack.common.ts | 110 +++++++++++++++++++++++++--- 8 files changed, 111 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index f71c731af..b993c223c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "eslint": "^8.36.0", "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", + "html-webpack-plugin": "^5.5.1", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "mini-css-extract-plugin": "^2.7.5", diff --git a/public/app.html b/public/app.html index 99444ee94..fa7b305eb 100644 --- a/public/app.html +++ b/public/app.html @@ -3,12 +3,10 @@ -
- \ No newline at end of file diff --git a/public/guide.html b/public/guide.html index b0d70fceb..aa3782b78 100644 --- a/public/guide.html +++ b/public/guide.html @@ -7,13 +7,13 @@ - - - - - -
- - - + + + + + +
+ + + \ No newline at end of file diff --git a/public/popup.html b/public/popup.html index 2bdda1a63..3f771cfc4 100644 --- a/public/popup.html +++ b/public/popup.html @@ -10,7 +10,6 @@ - @@ -149,6 +148,5 @@ - \ No newline at end of file diff --git a/src/guide/layout/header/header.sass b/src/guide/layout/header/header.sass index c685da29e..750e4c101 100644 --- a/src/guide/layout/header/header.sass +++ b/src/guide/layout/header/header.sass @@ -63,7 +63,7 @@ $localeIconSize: 19px width: $localeIconSize height: $localeIconSize span - font-size: calc( $localeIconSize - 3 ) + font-size: calc( $localeIconSize - 3px ) padding-left: 3px line-height: $localeIconSize color: var(--el-text-color-regular) diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass index c806d1332..d8ee48aeb 100644 --- a/src/guide/style/light-theme.sass +++ b/src/guide/style/light-theme.sass @@ -6,7 +6,7 @@ */ \:root - --el-menu-bg-color: #1d222d + --el-menu-bg-color: var(--el-fill-color-blank) --el-menu-text-color: #c1c6c8 --el-menu-hover-bg-color: #262f3e --guide-container-bg-color: var(--el-fill-color-blank) diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index 7ba5c9ea8..a3a653f6b 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -8,7 +8,7 @@ import metaMessages, { MetaMessage } from "../message/common/meta" import contextMenusMessages, { ContextMenusMessage } from "../message/common/context-menus" import initialMessages, { InitialMessage } from "../message/common/initial" -import baseMessages, { BaseMessage } from "@i18n/message/common/base" +import baseMessages, { BaseMessage } from "../message/common/base" export type ChromeMessage = { meta: MetaMessage diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 2dc5af0c0..6eb360103 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -1,12 +1,14 @@ import path from "path" import GenerateJsonPlugin from "generate-json-webpack-plugin" import CopyWebpackPlugin from "copy-webpack-plugin" -import webpack from "webpack" +import webpack, { Chunk } from "webpack" // Generate json files import manifest from "../src/manifest" import i18nChrome from "../src/i18n/chrome" import tsConfig from '../tsconfig.json' import MiniCssExtractPlugin from "mini-css-extract-plugin" +import HtmlWebpackPlugin from "html-webpack-plugin" + const tsPathAlias = tsConfig.compilerOptions.paths const generateJsonPlugins = [ @@ -41,16 +43,40 @@ Object.entries(tsPathAlias).forEach(([alias, sourceArr]) => { console.log("Alias of typescript: ") console.log(resolveAlias) +type EntryConfig = { + name: string + path: string + chunkExclusive?: boolean +} + +const entryConfigs: EntryConfig[] = [{ + name: 'background', + path: './src/background', + chunkExclusive: true, +}, { + name: 'content_scripts', + path: './src/content-script', + chunkExclusive: true, +}, { + name: 'popup', + path: './src/popup', +}, { + name: 'app', + path: './src/app', +}, { + name: 'guide', + path: './src/guide', +}] + +const EXCLUDE_CHUNK_ENTRY = entryConfigs.filter(({ chunkExclusive }) => chunkExclusive).map(({ name }) => name) + +const excludeChunk = (chunk: Chunk) => !EXCLUDE_CHUNK_ENTRY.includes(chunk.name) + const staticOptions: webpack.Configuration = { - entry: { - background: './src/background', - content_scripts: './src/content-script', - // The entrance of popup page - popup: './src/popup', - // The entrance of app page - app: './src/app', - // The entrance of guide page - guide: './src/guide', + entry() { + const entry = {} + entryConfigs.forEach(({ name, path }) => entry[name] = path) + return entry }, output: { filename: '[name].js', @@ -91,7 +117,49 @@ const staticOptions: webpack.Configuration = { assert: false, // fallbacks of axios's dependencies end } - } + }, + optimization: { + splitChunks: { + cacheGroups: { + echarts: { + name: 'echarts', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/, + }, + // @vue & vue-router + vue: { + name: 'vue', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]@?vue(use)?(-router)?[\\/]/, + }, + element: { + name: 'element', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]@?element-plus[\\/]/, + }, + psl: { + name: 'psl', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]psl[\\/]/, + }, + lodash: { + name: 'lodash', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]lodash[\\/]/, + }, + axios: { + name: 'axios', + chunks: excludeChunk, + test: /[\\/]node_modules[\\/]axios[\\/]/, + }, + common: { + name: 'common', + chunks: excludeChunk, + test: /[\\/]src[\\/](service|database|util)[\\/]/, + }, + } + }, + }, } const optionGenerator = (outputPath: string, manifestHooker?: (manifest: chrome.runtime.ManifestV2) => void) => { @@ -101,10 +169,28 @@ const optionGenerator = (outputPath: string, manifestHooker?: (manifest: chrome. // copy static resources new CopyWebpackPlugin({ patterns: [ - { from: path.join(__dirname, '..', 'public'), to: path.join(outputPath, 'static') } + { + from: path.join(__dirname, '..', 'public', 'images'), + to: path.join(outputPath, 'static', 'images'), + } ] }), new MiniCssExtractPlugin(), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'app.html'), + filename: path.join('static', 'app.html'), + chunks: ['app'], + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'guide.html'), + filename: path.join('static', 'guide.html'), + chunks: ['guide'], + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '..', 'public', 'popup.html'), + filename: path.join('static', 'popup.html'), + chunks: ['popup'], + }), ] return { ...staticOptions, From 7ecbd47b6a38960abbd9d28708139c85af91bbef Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 12 May 2023 00:29:11 +0800 Subject: [PATCH 146/168] Fix styles caused by chunks split --- src/guide/style/light-theme.sass | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass index d8ee48aeb..a48602d7e 100644 --- a/src/guide/style/light-theme.sass +++ b/src/guide/style/light-theme.sass @@ -6,8 +6,5 @@ */ \:root - --el-menu-bg-color: var(--el-fill-color-blank) - --el-menu-text-color: #c1c6c8 - --el-menu-hover-bg-color: #262f3e --guide-container-bg-color: var(--el-fill-color-blank) --guide-article-title-color: #222222 From 3e0102da7151bec4304d3e4298d0909527e48661 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 12 May 2023 00:44:32 +0800 Subject: [PATCH 147/168] Change app name and description --- src/i18n/message/common/meta.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index c26eb7266..f271c248e 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -28,13 +28,13 @@ const _default: Messages = { }, ja: { name: 'Web時間統計', - marketName: 'Web時間統計', - description: '最高のオンライン時間統計ツールを作成します。', + marketName: 'ウェブタイムトラッカー', + description: '最高のウェブタイムトラッカーになるために。', }, en: { - name: 'Timer', - marketName: 'Timer - Browsing Time & Visit count', - description: 'To be the BEST web timer.', + name: 'Time Tracker', + marketName: 'Time Tracker', + description: 'To be the BEST webtime tracker.', slogan: SLOGAN_EN, }, } From 2488470299e77d3d40d409c6ea2fe7e24fa36289 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 12 May 2023 17:00:12 +0800 Subject: [PATCH 148/168] v1.8.1 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b993c223c..31ba7eacd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.8.0", + "version": "1.8.1", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -68,4 +68,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} From 5d5b89118655043f1af0572638b72acaafbb1c4f Mon Sep 17 00:00:00 2001 From: ZHY Date: Mon, 15 May 2023 15:22:43 +0800 Subject: [PATCH 149/168] Remove default locale of firefox url --- src/util/constant/url.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 199626a81..bb10575bb 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -8,7 +8,7 @@ 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/' +export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/firefox/addon/web%E6%99%82%E9%96%93%E7%B5%B1%E8%A8%88/' export const CHROME_HOMEPAGE = 'https://chrome.google.com/webstore/detail/%E7%BD%91%E8%B4%B9%E5%BE%88%E8%B4%B5-%E4%B8%8A%E7%BD%91%E6%97%B6%E9%97%B4%E7%BB%9F%E8%AE%A1/dkdhhcbjijekmneelocdllcldcpmekmm' export const EDGE_HOMEPAGE = 'https://microsoftedge.microsoft.com/addons/detail/timer-the-web-time-is-e/fepjgblalcnepokjblgbgmapmlkgfahc' @@ -135,4 +135,4 @@ export const CROWDIN_PROJECT_ID = 516822 * * @since 1.4.0 */ -export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' \ No newline at end of file +export const CROWDIN_HOMEPAGE = 'https://crowdin.com/project/timer-chrome-edge-firefox' From 67ef23db7f15c44d525318946d8125597f82a9dd Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 15 May 2023 21:36:09 +0800 Subject: [PATCH 150/168] Split i18n resources into json file --- script/crowdin/common.ts | 74 +--- script/crowdin/export-translation.ts | 4 +- script/crowdin/sync-source.ts | 8 +- script/crowdin/sync-translation.ts | 17 +- src/i18n/message/app/analysis-resource.json | 134 +++++++ src/i18n/message/app/analysis.ts | 137 +------ src/i18n/message/app/confirm-resource.json | 18 + src/i18n/message/app/confirm.ts | 21 +- src/i18n/message/app/dashboard-resource.json | 106 ++++++ src/i18n/message/app/dashboard.ts | 109 +----- .../message/app/data-manage-resource.json | 98 +++++ src/i18n/message/app/data-manage.ts | 101 +---- src/i18n/message/app/habit-resource.json | 102 +++++ src/i18n/message/app/habit.ts | 105 +----- src/i18n/message/app/help-us-resource.json | 28 ++ src/i18n/message/app/help-us.ts | 31 +- src/i18n/message/app/limit-resource.json | 154 ++++++++ src/i18n/message/app/limit.ts | 157 +------- src/i18n/message/app/menu-resource.json | 82 ++++ src/i18n/message/app/menu.ts | 85 +---- src/i18n/message/app/merge-rule-resource.json | 62 ++++ src/i18n/message/app/merge-rule.ts | 65 +--- src/i18n/message/app/operation-resource.json | 26 ++ src/i18n/message/app/operation.ts | 29 +- src/i18n/message/app/option-resource.json | 347 +++++++++++++++++ src/i18n/message/app/option.ts | 350 +----------------- src/i18n/message/app/report-resource.json | 130 +++++++ src/i18n/message/app/report.ts | 133 +------ .../message/app/site-manage-resource.json | 193 ++++++++++ src/i18n/message/app/site-manage.ts | 196 +--------- .../message/app/time-format-resource.json | 26 ++ src/i18n/message/app/time-format.ts | 29 +- src/i18n/message/app/whitelist-resource.json | 42 +++ src/i18n/message/app/whitelist.ts | 45 +-- src/i18n/message/common/base-resource.json | 22 ++ src/i18n/message/common/base.ts | 25 +- .../message/common/calendar-resource.json | 26 ++ src/i18n/message/common/calendar.ts | 29 +- .../common/content-script-resource.json | 38 ++ src/i18n/message/common/content-script.ts | 41 +- .../common/context-menus-resource.json | 30 ++ src/i18n/message/common/context-menus.ts | 33 +- src/i18n/message/common/initial-resource.json | 34 ++ src/i18n/message/common/initial.ts | 37 +- src/i18n/message/common/item-resource.json | 74 ++++ src/i18n/message/common/item.ts | 77 +--- src/i18n/message/common/locale-resource.json | 84 +++++ src/i18n/message/common/locale.ts | 87 +---- src/i18n/message/common/merge-resource.json | 26 ++ src/i18n/message/common/meta-resource.json | 24 ++ src/i18n/message/common/meta.ts | 27 +- .../common/popup-duration-resource.json | 26 ++ src/i18n/message/common/popup-duration.ts | 29 +- src/i18n/message/guide/app-resource.json | 30 ++ src/i18n/message/guide/app.ts | 35 +- src/i18n/message/guide/backup-resource.json | 78 ++++ src/i18n/message/guide/backup.ts | 83 +---- src/i18n/message/guide/home-resource.json | 22 ++ src/i18n/message/guide/home.ts | 25 +- src/i18n/message/guide/layout-resource.json | 38 ++ src/i18n/message/guide/layout.ts | 41 +- src/i18n/message/guide/limit-resource.json | 46 +++ src/i18n/message/guide/limit.ts | 51 +-- src/i18n/message/guide/merge-resource.json | 130 +++++++ src/i18n/message/guide/merge.ts | 145 +------- src/i18n/message/guide/privacy-resource.json | 138 +++++++ src/i18n/message/guide/privacy.ts | 141 +------ src/i18n/message/guide/start-resource.json | 46 +++ src/i18n/message/guide/start.ts | 49 +-- src/i18n/message/guide/virtual-resource.json | 46 +++ src/i18n/message/guide/virtual.ts | 51 +-- src/i18n/message/popup/chart-resource.json | 86 +++++ src/i18n/message/popup/chart.ts | 89 +---- 73 files changed, 2722 insertions(+), 2661 deletions(-) create mode 100644 src/i18n/message/app/analysis-resource.json create mode 100644 src/i18n/message/app/confirm-resource.json create mode 100644 src/i18n/message/app/dashboard-resource.json create mode 100644 src/i18n/message/app/data-manage-resource.json create mode 100644 src/i18n/message/app/habit-resource.json create mode 100644 src/i18n/message/app/help-us-resource.json create mode 100644 src/i18n/message/app/limit-resource.json create mode 100644 src/i18n/message/app/menu-resource.json create mode 100644 src/i18n/message/app/merge-rule-resource.json create mode 100644 src/i18n/message/app/operation-resource.json create mode 100644 src/i18n/message/app/option-resource.json create mode 100644 src/i18n/message/app/report-resource.json create mode 100644 src/i18n/message/app/site-manage-resource.json create mode 100644 src/i18n/message/app/time-format-resource.json create mode 100644 src/i18n/message/app/whitelist-resource.json create mode 100644 src/i18n/message/common/base-resource.json create mode 100644 src/i18n/message/common/calendar-resource.json create mode 100644 src/i18n/message/common/content-script-resource.json create mode 100644 src/i18n/message/common/context-menus-resource.json create mode 100644 src/i18n/message/common/initial-resource.json create mode 100644 src/i18n/message/common/item-resource.json create mode 100644 src/i18n/message/common/locale-resource.json create mode 100644 src/i18n/message/common/merge-resource.json create mode 100644 src/i18n/message/common/meta-resource.json create mode 100644 src/i18n/message/common/popup-duration-resource.json create mode 100644 src/i18n/message/guide/app-resource.json create mode 100644 src/i18n/message/guide/backup-resource.json create mode 100644 src/i18n/message/guide/home-resource.json create mode 100644 src/i18n/message/guide/layout-resource.json create mode 100644 src/i18n/message/guide/limit-resource.json create mode 100644 src/i18n/message/guide/merge-resource.json create mode 100644 src/i18n/message/guide/privacy-resource.json create mode 100644 src/i18n/message/guide/start-resource.json create mode 100644 src/i18n/message/guide/virtual-resource.json create mode 100644 src/i18n/message/popup/chart-resource.json diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 736ce1503..5e64b7f28 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -34,17 +34,18 @@ export const localeOf = (crowdinLang: CrowdinLanguage) => CROWDIN_I18N_MAP[crowd const IGNORED_FILE: Partial<{ [dir in Dir]: string[] }> = { common: [ // Strings for market - 'meta.ts', + 'meta', // Name of locales - 'locale.ts', + 'locale', ] } -export function isIgnored(dir: Dir, tsFilename: string) { - return !!IGNORED_FILE[dir]?.includes(tsFilename) +export function isIgnored(dir: Dir, fileName: string) { + return !!IGNORED_FILE[dir]?.includes(fileName) } -const MSG_BASE = path.join(__dirname, '..', '..', 'src', 'i18n', 'message') +export const MSG_BASE = path.join(__dirname, '..', '..', 'src', 'i18n', 'message') +export const RSC_FILE_SUFFIX = "-resource.json" /** * Read all messages from source file @@ -58,21 +59,23 @@ export async function readAllMessages(dir: Dir): Promise { - if (!file.endsWith('.ts')) { - return - } - if (file === 'index.ts') { + if (!file.endsWith(RSC_FILE_SUFFIX)) { return } const message = (await import(`@i18n/message/${dir}/${file}`))?.default as Messages - message && (result[file] = message) + const name = file.replace(RSC_FILE_SUFFIX, '') + message && (result[name] = message) return })) return result } /** - * Merge crowdin message into locale codes + * Merge crowdin message into locale resource json files + * + * @param dir dir + * @param filename the name of json file + * @param messages crowdin messages */ export async function mergeMessage( dir: Dir, @@ -108,18 +111,7 @@ export async function mergeMessage( } }) - const existFile = fs.readFileSync(filePath, { encoding: 'utf-8' }) - const pattern = /(const|let|var) _default(.*)=\s*\{\s*(\n?.*\n)+\}/ - const patternRes = pattern.exec(existFile) - const existDefault = patternRes?.[0] - if (!existDefault) { - exitWith(`Failed to find: ${pattern} in ${filePath}`) - } - const index = existFile.indexOf(existDefault) - const pre = existFile.substring(0, index) - const suffix = existFile.substring(index + existDefault.length) - const newDefault = generateDefault(existDefault, existMessages) - const newFileContent = pre + newDefault + suffix + const newFileContent = JSON.stringify(existMessages, null, 4) fs.writeFileSync(filePath, newFileContent, { encoding: 'utf-8' }) } @@ -143,42 +135,6 @@ function checkPlaceholder(translated: string, source: string) { return true } -const INDENTATION_UNIT = ' ' - -function generateDefault(existDetault: string, messages: Messages): string { - let codeLines = /(const|let|var) _default(.*)=\s*\{/.exec(existDetault)[0] - codeLines += '\n' - codeLines += generateFieldLines(messages, INDENTATION_UNIT) - codeLines += '\n}' - return codeLines -} - -function generateFieldLines(messages: Object, indentation: string): string { - const lines = [] - Object.entries(messages).forEach(([key, value]) => { - let line = undefined - if (typeof value === 'object') { - const subCodeLines = generateFieldLines(value, indentation + INDENTATION_UNIT) - line = `${indentation}${key}: {\n${subCodeLines}\n${indentation}}` - } else { - 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) - }) - let codeLines = lines.join(',\n') - if (codeLines) { - // Add comma at the end of last line - codeLines += ',' - } - return codeLines -} - - function fillItem(fields: string[], index: number, obj: Object, text: string) { const field = fields[index] if (index === fields.length - 1) { diff --git a/script/crowdin/export-translation.ts b/script/crowdin/export-translation.ts index a44193e4d..ee50d9b09 100644 --- a/script/crowdin/export-translation.ts +++ b/script/crowdin/export-translation.ts @@ -1,6 +1,6 @@ import { SourceFilesModel } from "@crowdin/crowdin-api-client" import { CrowdinClient, getClientFromEnv } from "./client" -import { ALL_DIRS, ALL_TRANS_LOCALES, checkMainBranch, crowdinLangOf, mergeMessage } from "./common" +import { ALL_DIRS, ALL_TRANS_LOCALES, RSC_FILE_SUFFIX, checkMainBranch, crowdinLangOf, mergeMessage } from "./common" async function processFile(client: CrowdinClient, file: SourceFilesModel.File, dir: Dir): Promise { const itemSets: Partial> = {} @@ -9,7 +9,7 @@ async function processFile(client: CrowdinClient, file: SourceFilesModel.File, d const items: ItemSet = await client.downloadTranslations(file.id, lang) items && Object.keys(items).length && (itemSets[locale] = items) } - await mergeMessage(dir, file.name.replace('.json', '.ts'), itemSets) + await mergeMessage(dir, file.name.replace('.json', RSC_FILE_SUFFIX), itemSets) } async function processDir(client: CrowdinClient, branch: SourceFilesModel.Branch, dir: Dir): Promise { diff --git a/script/crowdin/sync-source.ts b/script/crowdin/sync-source.ts index e5bbe27b1..5e8f0128e 100644 --- a/script/crowdin/sync-source.ts +++ b/script/crowdin/sync-source.ts @@ -63,12 +63,12 @@ async function processByDir(client: CrowdinClient, dir: Dir, branch: SourceFiles console.log("Exists file count: " + existFiles.length) const existFileNameMap = groupBy(existFiles, f => f.name, l => l[0]) // 4. create new files - for (const [tsFilename, msg] of Object.entries(messages)) { - if (isIgnored(dir, tsFilename)) { - console.log(`Ignored file: ${dir}/${tsFilename}`) + for (const [fileName, msg] of Object.entries(messages)) { + if (isIgnored(dir, fileName)) { + console.log(`Ignored file: ${dir}/${fileName}`) continue } - const crwodinFilename = tsFilename.replace('.ts', '.json') + const crwodinFilename = fileName + '.json' const fileContent = transMsg(msg[SOURCE_LOCALE]) let existFile = existFileNameMap[crwodinFilename] if (!existFile) { diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index 08b9ebf3d..874cc03a6 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -42,16 +42,16 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo const files = await client.listFilesByDirectory(directory.id) console.log(`find ${files.length} files of ${dir}`) const fileMap = groupBy(files, f => f.name, l => l[0]) - for (const [tsFilename, message] of Object.entries(messages)) { - console.log(`Start to sync translations of ${dir}/${tsFilename}`) - if (isIgnored(dir, tsFilename)) { - console.log("Ignored file: " + tsFilename) + for (const [fileName, message] of Object.entries(messages)) { + console.log(`Start to sync translations of ${dir}/${fileName}`) + if (isIgnored(dir, fileName)) { + console.log("Ignored file: " + fileName) continue } - const filename = tsFilename.replace('.ts', '.json') - const crowdinFile = fileMap[filename] + const crowdinFileName = fileName + '.json' + const crowdinFile = fileMap[crowdinFileName] if (!crowdinFile) { - console.log(`Failed to find file: dir=${dir}, filename=${tsFilename}`) + console.log(`Failed to find file: dir=${dir}, filename=${fileName}`) continue } @@ -71,9 +71,6 @@ async function main() { const client = getClientFromEnv() const branch = await checkMainBranch(client) - for (let i = 0; i < ALL_DIRS.length; i++) { - - } for (const dir of ALL_DIRS) { await processDir(client, dir, branch) } diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json new file mode 100644 index 000000000..073d8d7b3 --- /dev/null +++ b/src/i18n/message/app/analysis-resource.json @@ -0,0 +1,134 @@ +{ + "zh_CN": { + "common": { + "focusTotal": "总计浏览时长", + "visitTotal": "总计访问次数", + "ringGrowth": "与上期相比 {value}", + "merged": "合并", + "virtual": "自定义", + "hostPlaceholder": "搜索你想分析的站点", + "emptyDesc": "未选择站点" + }, + "summary": { + "title": "数据总览", + "day": "总计活跃天数", + "firstDay": "首次访问 {value}" + }, + "trend": { + "title": "区间趋势", + "startDate": "开始日期", + "endDate": "结束日期", + "lastWeek": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last90Days": "最近 90 天", + "activeDay": "活跃天数", + "totalDay": "区间总天数", + "maxFocus": "单日最大浏览时长", + "averageFocus": "单日平均浏览时长", + "maxVisit": "单日最大访问次数", + "averageVisit": "单日平均访问次数", + "focusTitle": "浏览时长趋势", + "visitTitle": "访问次数趋势" + } + }, + "zh_TW": { + "common": { + "focusTotal": "總計瀏覽時長", + "visitTotal": "總計訪問次數", + "ringGrowth": "與前期相比 {value}", + "merged": "合並", + "virtual": "自定義", + "hostPlaceholder": "蒐索你想分析的站點", + "emptyDesc": "未選擇站點" + }, + "summary": { + "title": "數據總覽", + "day": "總計活躍天數", + "firstDay": "首次訪問 {value}" + }, + "trend": { + "title": "區間趨勢", + "startDate": "開始日期", + "endDate": "結束日期", + "lastWeek": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last90Days": "最近 90 天", + "activeDay": "活躍天數", + "totalDay": "區間總天數", + "maxFocus": "單日最大瀏覽時長", + "averageFocus": "單日平均瀏覽時長", + "maxVisit": "單日最大訪問次數", + "averageVisit": "單日平均訪問次數", + "focusTitle": "瀏覽時長趨勢", + "visitTitle": "訪問次數趨勢" + } + }, + "en": { + "common": { + "focusTotal": "Total browsing time", + "visitTotal": "Total visits", + "ringGrowth": "{value} compared to the previous period", + "merged": "Merged", + "virtual": "Virtual", + "hostPlaceholder": "Search for a site to analyze", + "emptyDesc": "No site selected" + }, + "summary": { + "title": "Summary", + "day": "Total active days", + "firstDay": "First visit {value}" + }, + "trend": { + "title": "Trends", + "startDate": "Start date", + "endDate": "End date", + "lastWeek": "Last week", + "last15Days": "Last 15 days", + "last30Days": "Last 30 days", + "last90Days": "Last 90 days", + "activeDay": "Active days", + "totalDay": "Period days", + "maxFocus": "Daily maximum browsing time", + "averageFocus": "Daily average browsing time", + "maxVisit": "Daily maximum visits", + "averageVisit": "Daily average visits", + "focusTitle": "Browsing Time Trends", + "visitTitle": "Visit Trends" + } + }, + "ja": { + "common": { + "focusTotal": "総閲覧時間", + "visitTotal": "総訪問数", + "ringGrowth": "前期比 {value}", + "merged": "合并", + "virtual": "カスタマイズ", + "hostPlaceholder": "ドメイン名を検索", + "emptyDesc": "サイトは空です" + }, + "summary": { + "title": "Summary", + "day": "Total active days", + "firstDay": "First visit {value}" + }, + "trend": { + "title": "レンジトレンド", + "startDate": "開始日", + "endDate": "終了日", + "lastWeek": "過去 7 日間", + "last15Days": "過去 15 日間", + "last30Days": "過去 30 日間", + "last90Days": "過去 90 日間", + "activeDay": "アクティブな日", + "totalDay": "間隔の合計日数", + "maxFocus": "1 日の最大閲覧時間", + "averageFocus": "1 日あたりの平均閲覧時間", + "maxVisit": "1 日あたりの最大訪問数", + "averageVisit": "1 日あたりの平均訪問数", + "focusTitle": "タイム トレンドの閲覧", + "visitTitle": "訪問数の傾向" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/analysis.ts b/src/i18n/message/app/analysis.ts index 21b186a67..4246ffc1b 100644 --- a/src/i18n/message/app/analysis.ts +++ b/src/i18n/message/app/analysis.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './analysis-resource.json' + export type AnalysisMessage = { common: { focusTotal: string @@ -39,139 +41,6 @@ export type AnalysisMessage = { } } -const _default: Messages = { - zh_CN: { - common: { - focusTotal: '总计浏览时长', - visitTotal: '总计访问次数', - ringGrowth: '与上期相比 {value}', - merged: '合并', - virtual: '自定义', - hostPlaceholder: '搜索你想分析的站点', - emptyDesc: '未选择站点' - }, - summary: { - title: '数据总览', - day: '总计活跃天数', - firstDay: '首次访问 {value}', - }, - trend: { - title: '区间趋势', - startDate: '开始日期', - endDate: '结束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - activeDay: '活跃天数', - totalDay: '区间总天数', - maxFocus: '单日最大浏览时长', - averageFocus: '单日平均浏览时长', - maxVisit: '单日最大访问次数', - averageVisit: '单日平均访问次数', - focusTitle: '浏览时长趋势', - visitTitle: '访问次数趋势', - } - }, - zh_TW: { - common: { - focusTotal: '總計瀏覽時長', - visitTotal: '總計訪問次數', - ringGrowth: '與前期相比 {value}', - merged: '合並', - virtual: '自定義', - hostPlaceholder: '蒐索你想分析的站點', - emptyDesc: '未選擇站點', - }, - summary: { - title: '數據總覽', - day: '總計活躍天數', - firstDay: '首次訪問 {value}', - }, - trend: { - title: '區間趨勢', - startDate: '開始日期', - endDate: '結束日期', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last90Days: '最近 90 天', - activeDay: '活躍天數', - totalDay: '區間總天數', - maxFocus: '單日最大瀏覽時長', - averageFocus: '單日平均瀏覽時長', - maxVisit: '單日最大訪問次數', - averageVisit: '單日平均訪問次數', - focusTitle: '瀏覽時長趨勢', - visitTitle: '訪問次數趨勢', - } - }, - en: { - common: { - focusTotal: 'Total browsing time', - visitTotal: 'Total visits', - ringGrowth: '{value} compared to the previous period', - merged: 'Merged', - virtual: 'Virtual', - hostPlaceholder: 'Search for a site to analyze', - emptyDesc: 'No site selected', - }, - summary: { - title: 'Summary', - day: 'Total active days', - firstDay: 'First visit {value}', - }, - trend: { - title: 'Trends', - startDate: 'Start date', - endDate: 'End date', - lastWeek: 'Last week', - last15Days: 'Last 15 days', - last30Days: 'Last 30 days', - last90Days: 'Last 90 days', - activeDay: 'Active days', - totalDay: 'Period days', - maxFocus: 'Daily maximum browsing time', - averageFocus: 'Daily average browsing time', - maxVisit: 'Daily maximum visits', - averageVisit: 'Daily average visits', - focusTitle: 'Browsing Time Trends', - visitTitle: 'Visit Trends', - } - }, - ja: { - common: { - focusTotal: '総閲覧時間', - visitTotal: '総訪問数', - ringGrowth: '前期比 {value}', - merged: '合并', - virtual: 'カスタマイズ', - hostPlaceholder: 'ドメイン名を検索', - emptyDesc: 'サイトは空です', - }, - summary: { - title: 'Summary', - day: 'Total active days', - firstDay: 'First visit {value}', - }, - trend: { - title: 'レンジトレンド', - startDate: '開始日', - endDate: '終了日', - lastWeek: '過去 7 日間', - last15Days: '過去 15 日間', - last30Days: '過去 30 日間', - last90Days: '過去 90 日間', - activeDay: 'アクティブな日', - totalDay: '間隔の合計日数', - maxFocus: '1 日の最大閲覧時間', - averageFocus: '1 日あたりの平均閲覧時間', - maxVisit: '1 日あたりの最大訪問数', - averageVisit: '1 日あたりの平均訪問数', - focusTitle: 'タイム トレンドの閲覧', - visitTitle: '訪問数の傾向', - } - } -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/confirm-resource.json b/src/i18n/message/app/confirm-resource.json new file mode 100644 index 000000000..14d6af83b --- /dev/null +++ b/src/i18n/message/app/confirm-resource.json @@ -0,0 +1,18 @@ +{ + "zh_CN": { + "confirmMsg": "好的", + "cancelMsg": "不用了" + }, + "zh_TW": { + "confirmMsg": "好的", + "cancelMsg": "不用了" + }, + "en": { + "confirmMsg": "OK", + "cancelMsg": "NO!" + }, + "ja": { + "confirmMsg": "OK", + "cancelMsg": "キャンセル" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/confirm.ts b/src/i18n/message/app/confirm.ts index 956e81f86..3af780466 100644 --- a/src/i18n/message/app/confirm.ts +++ b/src/i18n/message/app/confirm.ts @@ -5,28 +5,13 @@ * https://opensource.org/licenses/MIT */ +import resource from './confirm-resource.json' + export type ConfirmMessage = { confirmMsg: string, cancelMsg: string } -const _default: Messages = { - zh_CN: { - confirmMsg: '好的', - cancelMsg: '不用了', - }, - zh_TW: { - confirmMsg: '好的', - cancelMsg: '不用了', - }, - en: { - confirmMsg: 'OK', - cancelMsg: 'NO!', - }, - ja: { - confirmMsg: 'OK', - cancelMsg: 'キャンセル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json new file mode 100644 index 000000000..a37950177 --- /dev/null +++ b/src/i18n/message/app/dashboard-resource.json @@ -0,0 +1,106 @@ +{ + "zh_CN": { + "heatMap": { + "title0": "近一年上网总时长超过 {hour} 小时", + "title1": "近一年上网总时长不足 1 小时", + "tooltip0": "{year}/{month}/{day} 浏览网页 {minute} 分钟", + "tooltip1": "{year}/{month}/{day} 浏览网页 {hour} 小时 {minute} 分钟" + }, + "topK": { + "title": "近 {day} 天最常访问 TOP {k}", + "tooltip": "访问 {host} {visit} 次" + }, + "indicator": { + "installedDays": "已使用 {number} 天", + "visitCount": "累计访问过 {site} 个网站共 {visit} 次", + "browsingTime": "浏览时长超过 {minute} 分钟", + "mostUse": "最喜欢在 {start} 点至 {end} 点之间打开浏览器" + }, + "weekOnWeek": { + "title": "近一周浏览时长环比变化 TOP {k}", + "lastBrowse": "上周浏览 {time}", + "thisBrowse": "本周浏览 {time}", + "wow": "环比{state} {delta}", + "increase": "增长", + "decline": "减少" + } + }, + "zh_TW": { + "heatMap": { + "title0": "近一年上網總時長超過 {hour} 小時", + "title1": "近一年上網總時長不足 1 小時", + "tooltip0": "{year}/{month}/{day} 瀏覽網頁 {minute} 分鐘", + "tooltip1": "{year}/{month}/{day} 瀏覽網頁 {hour} 小時 {minute} 分鐘" + }, + "topK": { + "title": "近 {day} 天最常拜訪 TOP {k}", + "tooltip": "拜訪 {host} {visit} 次" + }, + "indicator": { + "installedDays": "已使用 {number} 天", + "visitCount": "拜訪過 {site} 個網站總計 {visit} 次", + "browsingTime": "瀏覽網頁超過 {minute} 分鐘", + "mostUse": "最喜歡在 {start} 點至 {end} 點之間上網" + }, + "weekOnWeek": { + "title": "近一周瀏覽時長環比變化 TOP {k}", + "lastBrowse": "上週瀏覽 {time}", + "thisBrowse": "本週瀏覽 {time}", + "wow": "環比{state} {delta}", + "increase": "增長", + "decline": "減少" + } + }, + "en": { + "heatMap": { + "title0": "Browsed over {hour} hours in the last year", + "title1": "Browsed less than 1 hour last year", + "tooltip0": "Browsed for {minute} minutes on {month}/{day}/{year}", + "tooltip1": "Browsed for {hour} hours {minute} minutes on {month}/{day}/{year}" + }, + "topK": { + "title": "TOP {k} most visited in the past {day} days", + "tooltip": "Visited {host} {visit} times" + }, + "indicator": { + "installedDays": "Installed for {number} days", + "visitCount": "Visited {site} websites {visit} times", + "browsingTime": "Browsed over {minute} minutes", + "mostUse": "Favorite browsing between {start} and {end} o'clock" + }, + "weekOnWeek": { + "title": "TOP {k} week-on-week change of browsing time", + "lastBrowse": "Browsed {time} last week", + "thisBrowse": "Browsed {time} this week", + "wow": "{delta} {state}", + "increase": "increased", + "decline": "decreased" + } + }, + "ja": { + "heatMap": { + "title0": "過去1年間に {hour} 時間以上オンラインで過ごした", + "title1": "過去 1 年間にオンラインで費やした時間は 1 時間未満", + "tooltip0": "{year} 年 {month} 月 {day} 日 {minute} 分間ウェブを閲覧する", + "tooltip1": "{year} 年 {month} 月 {day} 日 ウェブを {hour} 時間 {minute} 分閲覧する" + }, + "topK": { + "title": "過去 {day} 日間に最も拜訪された TOP {k}", + "tooltip": "{host} {visit} 回拜訪" + }, + "indicator": { + "installedDays": "使用 {number} 日", + "visitCount": "{site} つのサイトへの合計 {visit} 回の拜訪", + "browsingTime": "{minute} 分以上ウェブを閲覧する", + "mostUse": "{start}:00 から {end}:00 までのお気に入りのインターネットアクセス" + }, + "weekOnWeek": { + "title": "週ごとの変更 TOP {k}", + "lastBrowse": "先週 {time} 閲覧", + "thisBrowse": "今週は {time} で閲覧", + "wow": "毎週 {delta} の {state}", + "increase": "増加", + "decline": "減らす" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/dashboard.ts b/src/i18n/message/app/dashboard.ts index 6c47609f1..a58175227 100644 --- a/src/i18n/message/app/dashboard.ts +++ b/src/i18n/message/app/dashboard.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './dashboard-resource.json' + export type DashboardMessage = { heatMap: { title0: string @@ -32,111 +34,6 @@ export type DashboardMessage = { } } -const _default: Messages = { - zh_CN: { - heatMap: { - title0: '近一年上网总时长超过 {hour} 小时', - title1: '近一年上网总时长不足 1 小时', - tooltip0: '{year}/{month}/{day} 浏览网页 {minute} 分钟', - tooltip1: '{year}/{month}/{day} 浏览网页 {hour} 小时 {minute} 分钟', - }, - topK: { - title: '近 {day} 天最常访问 TOP {k}', - tooltip: '访问 {host} {visit} 次', - }, - indicator: { - installedDays: '已使用 {number} 天', - visitCount: '累计访问过 {site} 个网站共 {visit} 次', - browsingTime: '浏览时长超过 {minute} 分钟', - mostUse: '最喜欢在 {start} 点至 {end} 点之间打开浏览器', - }, - weekOnWeek: { - title: '近一周浏览时长环比变化 TOP {k}', - lastBrowse: '上周浏览 {time}', - thisBrowse: '本周浏览 {time}', - wow: '环比{state} {delta}', - increase: '增长', - decline: '减少', - }, - }, - zh_TW: { - heatMap: { - title0: '近一年上網總時長超過 {hour} 小時', - title1: '近一年上網總時長不足 1 小時', - tooltip0: '{year}/{month}/{day} 瀏覽網頁 {minute} 分鐘', - tooltip1: '{year}/{month}/{day} 瀏覽網頁 {hour} 小時 {minute} 分鐘', - }, - topK: { - title: '近 {day} 天最常拜訪 TOP {k}', - tooltip: '拜訪 {host} {visit} 次', - }, - indicator: { - installedDays: '已使用 {number} 天', - visitCount: '拜訪過 {site} 個網站總計 {visit} 次', - browsingTime: '瀏覽網頁超過 {minute} 分鐘', - mostUse: '最喜歡在 {start} 點至 {end} 點之間上網', - }, - weekOnWeek: { - title: '近一周瀏覽時長環比變化 TOP {k}', - lastBrowse: '上週瀏覽 {time}', - thisBrowse: '本週瀏覽 {time}', - wow: '環比{state} {delta}', - increase: '增長', - decline: '減少', - }, - }, - en: { - heatMap: { - title0: 'Browsed over {hour} hours in the last year', - title1: 'Browsed less than 1 hour last year', - tooltip0: 'Browsed for {minute} minutes on {month}/{day}/{year}', - tooltip1: 'Browsed for {hour} hours {minute} minutes on {month}/{day}/{year}', - }, - topK: { - title: 'TOP {k} most visited in the past {day} days', - tooltip: 'Visited {host} {visit} times', - }, - indicator: { - installedDays: 'Installed for {number} days', - visitCount: 'Visited {site} websites {visit} times', - browsingTime: 'Browsed over {minute} minutes', - mostUse: 'Favorite browsing between {start} and {end} o\'clock', - }, - weekOnWeek: { - title: 'TOP {k} week-on-week change of browsing time', - lastBrowse: 'Browsed {time} last week', - thisBrowse: 'Browsed {time} this week', - wow: '{delta} {state}', - increase: 'increased', - decline: 'decreased', - }, - }, - ja: { - heatMap: { - title0: '過去1年間に {hour} 時間以上オンラインで過ごした', - title1: '過去 1 年間にオンラインで費やした時間は 1 時間未満', - tooltip0: '{year} 年 {month} 月 {day} 日 {minute} 分間ウェブを閲覧する', - tooltip1: '{year} 年 {month} 月 {day} 日 ウェブを {hour} 時間 {minute} 分閲覧する', - }, - topK: { - title: '過去 {day} 日間に最も拜訪された TOP {k}', - tooltip: '{host} {visit} 回拜訪', - }, - indicator: { - installedDays: '使用 {number} 日', - visitCount: '{site} つのサイトへの合計 {visit} 回の拜訪', - browsingTime: '{minute} 分以上ウェブを閲覧する', - mostUse: '{start}:00 から {end}:00 までのお気に入りのインターネットアクセス', - }, - weekOnWeek: { - title: '週ごとの変更 TOP {k}', - lastBrowse: '先週 {time} 閲覧', - thisBrowse: '今週は {time} で閲覧', - wow: '毎週 {delta} の {state}', - increase: '増加', - decline: '減らす', - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/app/data-manage-resource.json b/src/i18n/message/app/data-manage-resource.json new file mode 100644 index 000000000..eb3ca7cc9 --- /dev/null +++ b/src/i18n/message/app/data-manage-resource.json @@ -0,0 +1,98 @@ +{ + "zh_CN": { + "totalMemoryAlert": "浏览器为每个扩展提供 {size}MB 来存储本地数据", + "totalMemoryAlert1": "无法确定浏览器允许的最大可用内存", + "usedMemoryAlert": "当前已使用 {size}MB", + "operationAlert": "您可以删除那些无关紧要的数据,来减小内存空间", + "filterItems": "数据筛选", + "filterFocus": "当日阅览时间在 {start} 秒至 {end} 秒之间。", + "filterTime": "当日打开次数在 {start} 次至 {end} 次之间。", + "filterDate": "{picker} 产生的数据。", + "unlimited": "无限", + "dateShortcut": { + "tillYesterday": "直到昨天", + "till7DaysAgo": "直到7天前", + "till30DaysAgo": "直到30天前" + }, + "paramError": "参数错误,请检查!", + "deleteConfirm": "共筛选出 {count} 条数据,是否全部删除?", + "deleteSuccess": "删除成功", + "migrationAlert": "使用导入/导出在不同浏览器之间迁移数据", + "importError": "文件格式错误", + "migrated": "成功导入", + "operationCancel": "取消", + "operationConfirm": "确认" + }, + "zh_TW": { + "totalMemoryAlert": "瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據", + "totalMemoryAlert1": "無法確定瀏覽器允許的最大可用內存", + "usedMemoryAlert": "當前已使用 {size}MB", + "operationAlert": "您可以刪除那些無關緊要的數據,來減小內存空間", + "filterItems": "數據篩選", + "filterFocus": "當日閱覽時間在 {start} 秒至 {end} 秒之間。", + "filterTime": "當日打開次數在 {start} 次至 {end} 次之間。", + "filterDate": "{picker} 産生的數據。", + "unlimited": "無限", + "dateShortcut": { + "tillYesterday": "直到昨天", + "till7DaysAgo": "直到7天前", + "till30DaysAgo": "直到30天前" + }, + "paramError": "參數錯誤,請檢查!", + "deleteConfirm": "共篩選出 {count} 條數據,是否全部刪除?", + "deleteSuccess": "刪除成功", + "migrationAlert": "使用導入/導出在不同瀏覽器之間遷移數據", + "importError": "文件格式錯誤", + "migrated": "成功導入", + "operationCancel": "取消", + "operationConfirm": "確認" + }, + "en": { + "totalMemoryAlert": "The browser provides {size}MB to store local data for each extension", + "totalMemoryAlert1": "Unable to determine the maximum memory available allowed by the browser", + "usedMemoryAlert": "{size}MB is currently used", + "operationAlert": "You can delete those unimportant data to reduce memory usage", + "filterItems": "Filter data", + "filterFocus": "The browsing time of the day is between {start} seconds and {end} seconds", + "filterTime": "The number of visits for the day is between {start} and {end}", + "filterDate": "Recorded between {picker}", + "unlimited": "∞", + "dateShortcut": { + "tillYesterday": "Until yesterday", + "till7DaysAgo": "Until 7 days ago", + "till30DaysAgo": "Until 30 days ago" + }, + "paramError": "The parameter is wrong, please check!", + "deleteConfirm": "A total of {count} records have been filtered out. Do you want to delete them all?", + "deleteSuccess": "Deleted successfully!", + "migrationAlert": "Migrate data between browsers using import and export", + "importError": "Wrong file extension", + "migrated": "Imported successfully!", + "operationCancel": "Cancel", + "operationConfirm": "Confirm" + }, + "ja": { + "totalMemoryAlert": "ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します", + "totalMemoryAlert1": "ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません", + "usedMemoryAlert": "現在 {size}MB が使用されています", + "operationAlert": "これらの重要でないデータを削除して、メモリ使用量を減らすことができます", + "filterItems": "データフィルタリング", + "filterFocus": "当日の閲覧時間は、{start} 秒から {end} 秒の間です。", + "filterTime": "当日のオープン数は {start} から {end} の間です。", + "filterDate": "{picker} までに生成されたデータ", + "unlimited": "無制限", + "dateShortcut": { + "tillYesterday": "昨日まで", + "till7DaysAgo": "7日前まで", + "till30DaysAgo": "30日前まで" + }, + "paramError": "パラメータエラー、確認してください!", + "deleteConfirm": "合計 {count} 個のデータが除外されました。すべて削除しますか?", + "deleteSuccess": "正常に削除されました", + "migrationAlert": "インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します", + "importError": "ファイル形式エラー", + "migrated": "正常にインポートされました", + "operationCancel": "取消", + "operationConfirm": "確認" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/data-manage.ts b/src/i18n/message/app/data-manage.ts index 91023047f..1cac603f6 100644 --- a/src/i18n/message/app/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './data-manage-resource.json' + export type DataManageMessage = { totalMemoryAlert: string totalMemoryAlert1: string @@ -30,103 +32,6 @@ export type DataManageMessage = { } } -const _default: Messages = { - zh_CN: { - totalMemoryAlert: '浏览器为每个扩展提供 {size}MB 来存储本地数据', - totalMemoryAlert1: '无法确定浏览器允许的最大可用内存', - usedMemoryAlert: '当前已使用 {size}MB', - operationAlert: '您可以删除那些无关紧要的数据,来减小内存空间', - filterItems: '数据筛选', - filterFocus: '当日阅览时间在 {start} 秒至 {end} 秒之间。', - filterTime: '当日打开次数在 {start} 次至 {end} 次之间。', - filterDate: '{picker} 产生的数据。', - unlimited: '无限', - dateShortcut: { - tillYesterday: '直到昨天', - till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前', - }, - paramError: '参数错误,请检查!', - deleteConfirm: '共筛选出 {count} 条数据,是否全部删除?', - deleteSuccess: '删除成功', - migrationAlert: '使用导入/导出在不同浏览器之间迁移数据', - importError: '文件格式错误', - migrated: '成功导入', - operationCancel: '取消', - operationConfirm: '确认', - }, - zh_TW: { - totalMemoryAlert: '瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據', - totalMemoryAlert1: '無法確定瀏覽器允許的最大可用內存', - usedMemoryAlert: '當前已使用 {size}MB', - operationAlert: '您可以刪除那些無關緊要的數據,來減小內存空間', - filterItems: '數據篩選', - filterFocus: '當日閱覽時間在 {start} 秒至 {end} 秒之間。', - filterTime: '當日打開次數在 {start} 次至 {end} 次之間。', - filterDate: '{picker} 産生的數據。', - unlimited: '無限', - dateShortcut: { - tillYesterday: '直到昨天', - till7DaysAgo: '直到7天前', - till30DaysAgo: '直到30天前', - }, - paramError: '參數錯誤,請檢查!', - deleteConfirm: '共篩選出 {count} 條數據,是否全部刪除?', - deleteSuccess: '刪除成功', - migrationAlert: '使用導入/導出在不同瀏覽器之間遷移數據', - importError: '文件格式錯誤', - migrated: '成功導入', - operationCancel: '取消', - operationConfirm: '確認', - }, - en: { - totalMemoryAlert: 'The browser provides {size}MB to store local data for each extension', - totalMemoryAlert1: 'Unable to determine the maximum memory available allowed by the browser', - usedMemoryAlert: '{size}MB is currently used', - operationAlert: 'You can delete those unimportant data to reduce memory usage', - filterItems: 'Filter data', - filterFocus: 'The browsing time of the day is between {start} seconds and {end} seconds', - filterTime: 'The number of visits for the day is between {start} and {end}', - filterDate: 'Recorded between {picker}', - unlimited: '∞', - dateShortcut: { - tillYesterday: 'Until yesterday', - till7DaysAgo: 'Until 7 days ago', - till30DaysAgo: 'Until 30 days ago', - }, - paramError: 'The parameter is wrong, please check!', - deleteConfirm: 'A total of {count} records have been filtered out. Do you want to delete them all?', - deleteSuccess: 'Deleted successfully!', - migrationAlert: 'Migrate data between browsers using import and export', - importError: 'Wrong file extension', - migrated: 'Imported successfully!', - operationCancel: 'Cancel', - operationConfirm: 'Confirm', - }, - ja: { - totalMemoryAlert: 'ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します', - totalMemoryAlert1: 'ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません', - usedMemoryAlert: '現在 {size}MB が使用されています', - operationAlert: 'これらの重要でないデータを削除して、メモリ使用量を減らすことができます', - filterItems: 'データフィルタリング', - filterFocus: '当日の閲覧時間は、{start} 秒から {end} 秒の間です。', - filterTime: '当日のオープン数は {start} から {end} の間です。', - filterDate: '{picker} までに生成されたデータ', - unlimited: '無制限', - dateShortcut: { - tillYesterday: '昨日まで', - till7DaysAgo: '7日前まで', - till30DaysAgo: '30日前まで', - }, - paramError: 'パラメータエラー、確認してください!', - deleteConfirm: '合計 {count} 個のデータが除外されました。すべて削除しますか?', - deleteSuccess: '正常に削除されました', - migrationAlert: 'インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します', - importError: 'ファイル形式エラー', - migrated: '正常にインポートされました', - operationCancel: '取消', - operationConfirm: '確認', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json new file mode 100644 index 000000000..cc4d0995a --- /dev/null +++ b/src/i18n/message/app/habit-resource.json @@ -0,0 +1,102 @@ +{ + "zh_CN": { + "sizes": { + "fifteen": "每十五分钟统计一次", + "halfHour": "每半小时统计一次", + "hour": "每一小时统计一次", + "twoHour": "每两小时统计一次" + }, + "average": { + "label": "平均每天" + }, + "dateRange": { + "lastDay": "最近 24 小时", + "last3Days": "最近 3 天", + "lastWeek": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天" + }, + "chart": { + "title": "上网习惯统计", + "saveAsImageTitle": "保存", + "yAxisMin": "浏览时长 / 分钟", + "yAxisHour": "浏览时长 / 小时" + } + }, + "zh_TW": { + "sizes": { + "fifteen": "按十五分鐘統計", + "halfHour": "按半小時統計", + "hour": "按一小時統計", + "twoHour": "按兩小時統計" + }, + "average": { + "label": "平均每天" + }, + "dateRange": { + "lastDay": "最近 24 小時", + "last3Days": "最近 3 天", + "lastWeek": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天" + }, + "chart": { + "title": "上網習慣統計", + "saveAsImageTitle": "保存", + "yAxisMin": "瀏覽時長 / 分鐘", + "yAxisHour": "瀏覽時長 / 小時" + } + }, + "en": { + "sizes": { + "fifteen": "Per 15 minutes", + "halfHour": "Per half hour", + "hour": "Per one hour", + "twoHour": "Per two hours" + }, + "average": { + "label": "Daily average" + }, + "dateRange": { + "lastDay": "Last day", + "last3Days": "Last 3 days", + "lastWeek": "Last week", + "last15Days": "Last 15 days", + "last30Days": "Last 30 days", + "last60Days": "Last 60 days" + }, + "chart": { + "title": "Time-phased Statistics of Browsing Time", + "saveAsImageTitle": "Snapshot", + "yAxisMin": "Browsing Time / minute", + "yAxisHour": "Browsing Time / hour" + } + }, + "ja": { + "sizes": { + "fifteen": "15分で統計", + "halfHour": "30分で統計", + "hour": "1時間ごとの統計", + "twoHour": "2時間ごとの統計" + }, + "average": { + "label": "1日平均" + }, + "dateRange": { + "lastDay": "過去24時間", + "last3Days": "過去3日間", + "lastWeek": "先週", + "last15Days": "過去15日間", + "last30Days": "過去30日間", + "last60Days": "過去60日間" + }, + "chart": { + "title": "時系列の統計を閲覧する", + "saveAsImageTitle": "ダウンロード", + "yAxisMin": "閲覧時間/分", + "yAxisHour": "閲覧時間/時間" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/habit.ts b/src/i18n/message/app/habit.ts index 45835e60d..3e417ce87 100644 --- a/src/i18n/message/app/habit.ts +++ b/src/i18n/message/app/habit.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './habit-resource.json' + export type HabitMessage = { sizes: { fifteen: string @@ -31,107 +33,6 @@ export type HabitMessage = { } } -const _default: Messages = { - zh_CN: { - sizes: { - fifteen: '每十五分钟统计一次', - halfHour: '每半小时统计一次', - hour: '每一小时统计一次', - twoHour: '每两小时统计一次', - }, - average: { - label: '平均每天', - }, - dateRange: { - lastDay: '最近 24 小时', - last3Days: '最近 3 天', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last60Days: '最近 60 天', - }, - chart: { - title: '上网习惯统计', - saveAsImageTitle: '保存', - yAxisMin: '浏览时长 / 分钟', - yAxisHour: '浏览时长 / 小时', - }, - }, - zh_TW: { - sizes: { - fifteen: '按十五分鐘統計', - halfHour: '按半小時統計', - hour: '按一小時統計', - twoHour: '按兩小時統計', - }, - average: { - label: '平均每天', - }, - dateRange: { - lastDay: '最近 24 小時', - last3Days: '最近 3 天', - lastWeek: '最近 7 天', - last15Days: '最近 15 天', - last30Days: '最近 30 天', - last60Days: '最近 60 天', - }, - chart: { - title: '上網習慣統計', - saveAsImageTitle: '保存', - yAxisMin: '瀏覽時長 / 分鐘', - yAxisHour: '瀏覽時長 / 小時', - }, - }, - en: { - sizes: { - fifteen: 'Per 15 minutes', - halfHour: 'Per half hour', - hour: 'Per one hour', - twoHour: 'Per two hours', - }, - average: { - label: 'Daily average', - }, - dateRange: { - lastDay: 'Last day', - last3Days: 'Last 3 days', - lastWeek: 'Last week', - last15Days: 'Last 15 days', - last30Days: 'Last 30 days', - last60Days: 'Last 60 days', - }, - chart: { - title: 'Time-phased Statistics of Browsing Time', - saveAsImageTitle: 'Snapshot', - yAxisMin: 'Browsing Time / minute', - yAxisHour: 'Browsing Time / hour', - }, - }, - ja: { - sizes: { - fifteen: '15分で統計', - halfHour: '30分で統計', - hour: '1時間ごとの統計', - twoHour: '2時間ごとの統計', - }, - average: { - label: '1日平均', - }, - dateRange: { - lastDay: '過去24時間', - last3Days: '過去3日間', - lastWeek: '先週', - last15Days: '過去15日間', - last30Days: '過去30日間', - last60Days: '過去60日間', - }, - chart: { - title: '時系列の統計を閲覧する', - saveAsImageTitle: 'ダウンロード', - yAxisMin: '閲覧時間/分', - yAxisHour: '閲覧時間/時間', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/help-us-resource.json b/src/i18n/message/app/help-us-resource.json new file mode 100644 index 000000000..241387cf9 --- /dev/null +++ b/src/i18n/message/app/help-us-resource.json @@ -0,0 +1,28 @@ +{ + "zh_CN": { + "title": "欢迎一起来改善本地化翻译!", + "alert": { + "l1": "由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。", + "l2": "为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。", + "l3": "如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。", + "l4": "当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。" + }, + "button": "前往 Crowdin", + "loading": "正在查询翻译进度..." + }, + "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 50%, I will consider supporting it in this extension." + }, + "button": "Go Crowdin", + "loading": "Checking translation progress..." + }, + "zh_TW": { + "button": "前往 Crowdin", + "loading": "正在檢查翻譯進度..." + } +} \ No newline at end of file diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index d6e6cc564..ca8427e82 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './help-us-resource.json' + type _AlertLine = | 'l1' | 'l2' @@ -18,33 +20,6 @@ export type HelpUsMessage = { loading: string } -const _default: Messages = { - zh_CN: { - title: '欢迎一起来改善本地化翻译!', - alert: { - l1: '由于作者的语言能力,该扩展原生只支持简体中文和英语,其他语言要么缺失,要么就严重依赖机器翻译。', - l2: '为了能够提供更好的用户体验,我将其他语言的翻译任务托管在了 Crowdin 上。Crowdin 是一个对开源软件免费的翻译管理系统。', - l3: '如果您觉得这个扩展对您有用,并且您愿意完善它的文本翻译的话,可以点击下方按钮前往 Crowdin 上的项目主页。', - l4: '当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。', - }, - button: '前往 Crowdin', - loading: '正在查询翻译进度...', - }, - 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 50%, I will consider supporting it in this extension.', - }, - button: 'Go Crowdin', - loading: 'Checking translation progress...', - }, - zh_TW: { - button: '前往 Crowdin', - loading: '正在檢查翻譯進度...', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json new file mode 100644 index 000000000..2be5daf4c --- /dev/null +++ b/src/i18n/message/app/limit-resource.json @@ -0,0 +1,154 @@ +{ + "zh_CN": { + "conditionFilter": "输入网址,然后回车", + "filterDisabled": "过滤无效规则", + "item": { + "condition": "限制网址", + "time": "每日限制时长", + "waste": "今日浏览时长", + "enabled": "是否有效", + "delayAllowed": "再看 5 分钟", + "delayAllowedInfo": "上网时间超过限制时,点击【再看 5 分钟】短暂延时。如果关闭该功能则不能延时。", + "operation": "操作" + }, + "button": { + "add": "新增", + "test": "网址测试", + "testSimple": "测试", + "paste": "粘贴", + "save": "保存", + "delete": "删除", + "modify": "修改" + }, + "addTitle": "新增限制", + "useWildcard": "是否使用通配符", + "message": { + "saved": "保存成功", + "noUrl": "未填写限制网址", + "noTime": "未填写每日限制时长", + "deleteConfirm": "是否删除限制:{cond}?", + "deleted": "删除成功", + "noPermissionFirefox": "请先在插件管理页[about:addons]开启该插件的粘贴板权限", + "inputTestUrl": "请先输入需要测试的网址链接", + "clickTestButton": "输入完成后请点击【{buttonText}】按钮", + "noRuleMatched": "该网址未命中任何规则", + "rulesMatched": "该网址命中以下规则:" + }, + "testUrlLabel": "测试网址", + "urlPlaceholder": "请直接粘贴网址 ➡️" + }, + "zh_TW": { + "conditionFilter": "輸入網址,然後回車", + "filterDisabled": "過濾無效規則", + "item": { + "condition": "限製網址", + "time": "每日限製時長", + "waste": "今日瀏覽時長", + "enabled": "是否有效", + "delayAllowed": "再看 5 分鐘", + "delayAllowedInfo": "上網時間超過限製時,點擊【再看 5 分鐘】短暫延時。如果關閉該功能則不能延時。", + "operation": "操作" + }, + "button": { + "add": "新增", + "test": "網址測試", + "testSimple": "測試", + "paste": "粘貼", + "save": "保存", + "delete": "刪除", + "modify": "修改" + }, + "addTitle": "新增限製", + "useWildcard": "是否使用通配符", + "message": { + "saved": "保存成功", + "noUrl": "未填冩限製網址", + "noTime": "未填冩每日限製時長", + "deleteConfirm": "是否刪除限製:{cond}?", + "deleted": "刪除成功", + "noPermissionFirefox": "請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限", + "inputTestUrl": "請先輸入需要測試的網址鏈接", + "clickTestButton": "輸入完成後請點擊【{buttonText}】按鈕", + "noRuleMatched": "該網址未命中任何規則", + "rulesMatched": "該網址命中以下規則:" + }, + "urlPlaceholder": "請直接粘貼網址 ➡️", + "testUrlLabel": "測試網址" + }, + "en": { + "conditionFilter": "URL", + "filterDisabled": "Only enabled", + "item": { + "condition": "Restricted URL", + "time": "Daily time limit", + "waste": "Browsed today", + "enabled": "Enabled", + "delayAllowed": "More 5 minutes", + "delayAllowedInfo": "If it times out, allow a temporary delay of 5 minutes", + "operation": "Operations" + }, + "button": { + "add": "New", + "test": "Test URL", + "testSimple": "Test", + "paste": "Paste", + "save": "Save", + "delete": "Delete", + "modify": "Modify" + }, + "addTitle": "New", + "useWildcard": "Whether to use wildcard", + "message": { + "saved": "Saved successfully", + "noUrl": "Unfilled limited URL", + "noTime": "Unfilled limited time per day", + "deleteConfirm": "Do you want to delete the rule of {cond}?", + "deleted": "Deleted successfully", + "noPermissionFirefox": "Please enable the clipboard permission of this addon on the management page (about:addons) first", + "inputTestUrl": "Please enter the URL link to be tested first", + "clickTestButton": "After inputting, please click the button ({buttonText})", + "noRuleMatched": "The URL does not hit any rules", + "rulesMatched": "The URL hits the following rules:" + }, + "urlPlaceholder": "Please paste the URL directly ➡️", + "testUrlLabel": "Test URL" + }, + "ja": { + "conditionFilter": "URL", + "filterDisabled": "有效", + "item": { + "condition": "制限 URL", + "waste": "今日の時間を閲覧する", + "time": "1日あたりの制限", + "enabled": "有效", + "delayAllowed": "さらに5分間閲覧する", + "delayAllowedInfo": "時間が経過した場合は、一時的に5分遅らせることができます", + "operation": "操作" + }, + "button": { + "add": "新增", + "test": "テストURL", + "testSimple": "テスト", + "paste": "ペースト", + "save": "セーブ", + "delete": "削除", + "modify": "変更" + }, + "addTitle": "新增", + "useWildcard": "ワイルドカードを使用するかどうか", + "message": { + "noUrl": "埋められていない制限URL", + "noTime": "1日の制限時間を記入しない", + "saved": "正常に保存", + "deleteConfirm": "{cond} の制限を削除しますか?", + "deleted": "正常に削除", + "noPermissionFirefox": "最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください", + "inputTestUrl": "最初にテストする URL リンクを入力してください", + "clickTestButton": "入力後、ボタン({buttonText})をクリックしてください", + "noRuleMatched": "URL がどのルールとも一致しません", + "rulesMatched": "URL は次のルールに一致します。" + }, + "urlPlaceholder": "URLを直接貼り付けてください➡️", + "testUrlLabel": "テスト URL" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index 50a2fc8e4..9fa584364 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './limit-resource.json' + export type LimitMessage = { conditionFilter: string filterDisabled: string @@ -44,159 +46,6 @@ export type LimitMessage = { testUrlLabel: string } -const _default: Messages = { - zh_CN: { - conditionFilter: '输入网址,然后回车', - filterDisabled: '过滤无效规则', - item: { - condition: '限制网址', - time: '每日限制时长', - waste: '今日浏览时长', - enabled: '是否有效', - delayAllowed: '再看 5 分钟', - delayAllowedInfo: '上网时间超过限制时,点击【再看 5 分钟】短暂延时。如果关闭该功能则不能延时。', - operation: '操作', - }, - button: { - add: '新增', - test: '网址测试', - testSimple: '测试', - paste: '粘贴', - save: '保存', - delete: '删除', - modify: '修改', - }, - addTitle: '新增限制', - useWildcard: '是否使用通配符', - message: { - saved: '保存成功', - noUrl: '未填写限制网址', - noTime: '未填写每日限制时长', - deleteConfirm: '是否删除限制:{cond}?', - deleted: '删除成功', - noPermissionFirefox: '请先在插件管理页[about:addons]开启该插件的粘贴板权限', - inputTestUrl: '请先输入需要测试的网址链接', - clickTestButton: '输入完成后请点击【{buttonText}】按钮', - noRuleMatched: '该网址未命中任何规则', - rulesMatched: '该网址命中以下规则:', - }, - testUrlLabel: '测试网址', - urlPlaceholder: '请直接粘贴网址 ➡️', - }, - zh_TW: { - conditionFilter: '輸入網址,然後回車', - filterDisabled: '過濾無效規則', - item: { - condition: '限製網址', - time: '每日限製時長', - waste: '今日瀏覽時長', - enabled: '是否有效', - delayAllowed: '再看 5 分鐘', - delayAllowedInfo: '上網時間超過限製時,點擊【再看 5 分鐘】短暫延時。如果關閉該功能則不能延時。', - operation: '操作', - }, - button: { - add: '新增', - test: '網址測試', - testSimple: '測試', - paste: '粘貼', - save: '保存', - delete: '刪除', - modify: '修改', - }, - addTitle: '新增限製', - useWildcard: '是否使用通配符', - message: { - saved: '保存成功', - noUrl: '未填冩限製網址', - noTime: '未填冩每日限製時長', - deleteConfirm: '是否刪除限製:{cond}?', - deleted: '刪除成功', - noPermissionFirefox: '請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限', - inputTestUrl: '請先輸入需要測試的網址鏈接', - clickTestButton: '輸入完成後請點擊【{buttonText}】按鈕', - noRuleMatched: '該網址未命中任何規則', - rulesMatched: '該網址命中以下規則:', - }, - urlPlaceholder: '請直接粘貼網址 ➡️', - testUrlLabel: '測試網址', - }, - en: { - conditionFilter: 'URL', - filterDisabled: 'Only enabled', - item: { - condition: 'Restricted URL', - time: 'Daily time limit', - waste: 'Browsed today', - enabled: 'Enabled', - delayAllowed: 'More 5 minutes', - delayAllowedInfo: 'If it times out, allow a temporary delay of 5 minutes', - operation: 'Operations', - }, - button: { - add: 'New', - test: 'Test URL', - testSimple: 'Test', - paste: 'Paste', - save: 'Save', - delete: 'Delete', - modify: 'Modify', - }, - addTitle: 'New', - useWildcard: 'Whether to use wildcard', - message: { - saved: 'Saved successfully', - noUrl: 'Unfilled limited URL', - noTime: 'Unfilled limited time per day', - deleteConfirm: 'Do you want to delete the rule of {cond}?', - deleted: 'Deleted successfully', - noPermissionFirefox: 'Please enable the clipboard permission of this addon on the management page (about:addons) first', - inputTestUrl: 'Please enter the URL link to be tested first', - clickTestButton: 'After inputting, please click the button ({buttonText})', - noRuleMatched: 'The URL does not hit any rules', - rulesMatched: 'The URL hits the following rules:', - }, - urlPlaceholder: 'Please paste the URL directly ➡️', - testUrlLabel: 'Test URL', - }, - ja: { - conditionFilter: 'URL', - filterDisabled: '有效', - item: { - condition: '制限 URL', - waste: '今日の時間を閲覧する', - time: '1日あたりの制限', - enabled: '有效', - delayAllowed: 'さらに5分間閲覧する', - delayAllowedInfo: '時間が経過した場合は、一時的に5分遅らせることができます', - operation: '操作', - }, - button: { - add: '新增', - test: 'テストURL', - testSimple: 'テスト', - paste: 'ペースト', - save: 'セーブ', - delete: '削除', - modify: '変更', - }, - addTitle: '新增', - useWildcard: 'ワイルドカードを使用するかどうか', - message: { - noUrl: '埋められていない制限URL', - noTime: '1日の制限時間を記入しない', - saved: '正常に保存', - deleteConfirm: '{cond} の制限を削除しますか?', - deleted: '正常に削除', - noPermissionFirefox: '最初にプラグイン管理ページでプラグインのペーストボード権限を有効にしてください', - inputTestUrl: '最初にテストする URL リンクを入力してください', - clickTestButton: '入力後、ボタン({buttonText})をクリックしてください', - noRuleMatched: 'URL がどのルールとも一致しません', - rulesMatched: 'URL は次のルールに一致します。', - }, - urlPlaceholder: 'URLを直接貼り付けてください➡️', - testUrlLabel: 'テスト URL', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/menu-resource.json b/src/i18n/message/app/menu-resource.json new file mode 100644 index 000000000..84201f88d --- /dev/null +++ b/src/i18n/message/app/menu-resource.json @@ -0,0 +1,82 @@ +{ + "zh_CN": { + "dashboard": "仪表盘", + "data": "我的数据", + "dataReport": "报表明细", + "siteAnalysis": "站点分析", + "dataClear": "内存管理", + "additional": "附加功能", + "siteManage": "网站管理", + "whitelist": "白名单管理", + "mergeRule": "子域名合并", + "option": "扩展选项", + "behavior": "上网行为", + "habit": "上网习惯", + "limit": "每日时限设置", + "other": "其他", + "feedback": "有什么反馈吗?", + "rate": "打个分吧!", + "helpUs": "帮助我们~", + "userManual": "用户手册" + }, + "zh_TW": { + "dashboard": "儀錶盤", + "data": "我的數據", + "dataReport": "報表明細", + "siteAnalysis": "站點分析", + "dataClear": "內存管理", + "additional": "附加功能", + "siteManage": "網站管理", + "whitelist": "白名單管理", + "mergeRule": "子域名合並", + "option": "擴充選項", + "behavior": "上網行爲", + "habit": "上網習慣", + "limit": "每日時限設置", + "other": "其他", + "feedback": "有什麼反饋嗎?", + "rate": "打個分吧!", + "helpUs": "帮助我们~", + "userManual": "使用者手冊" + }, + "en": { + "dashboard": "Dashboard", + "data": "My Data", + "dataReport": "Record", + "siteAnalysis": "Site Analysis", + "dataClear": "Memory Situation", + "behavior": "User Behavior", + "habit": "Habits", + "limit": "Daily Limit", + "additional": "Additional Features", + "siteManage": "Site Management", + "whitelist": "Whitelist", + "mergeRule": "Merge-site Rules", + "other": "Other Features", + "option": "Options", + "feedback": "Feedback Questionnaire", + "rate": "Rate It", + "helpUs": "Help Us", + "userManual": "User Manual" + }, + "ja": { + "dashboard": "ダッシュボード", + "data": "私のデータ", + "dataReport": "報告する", + "siteAnalysis": "ウェブサイト分析", + "dataClear": "記憶状況", + "behavior": "ユーザーの行動", + "habit": "閲覧の習慣", + "limit": "閲覧の制限", + "additional": "その他の機能", + "siteManage": "ウェブサイト管理", + "whitelist": "Webホワイトリスト", + "mergeRule": "ドメイン合併", + "other": "その他の機能", + "option": "拡張設定", + "feedback": "フィードバックアンケート", + "rate": "それを評価", + "helpUs": "協力する", + "userManual": "ユーザーマニュアル" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index 73d1aa7ce..bdf518c5f 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './menu-resource.json' + export type MenuMessage = { dashboard: string data: string @@ -26,87 +28,6 @@ export type MenuMessage = { userManual: string } -const _default: Messages = { - zh_CN: { - dashboard: '仪表盘', - data: '我的数据', - dataReport: '报表明细', - siteAnalysis: '站点分析', - dataClear: '内存管理', - additional: '附加功能', - siteManage: '网站管理', - whitelist: '白名单管理', - mergeRule: '子域名合并', - option: '扩展选项', - behavior: '上网行为', - habit: '上网习惯', - limit: '每日时限设置', - other: '其他', - feedback: '有什么反馈吗?', - rate: '打个分吧!', - helpUs: '帮助我们~', - userManual: '用户手册', - }, - zh_TW: { - dashboard: '儀錶盤', - data: '我的數據', - dataReport: '報表明細', - siteAnalysis: '站點分析', - dataClear: '內存管理', - additional: '附加功能', - siteManage: '網站管理', - whitelist: '白名單管理', - mergeRule: '子域名合並', - option: '擴充選項', - behavior: '上網行爲', - habit: '上網習慣', - limit: '每日時限設置', - other: '其他', - feedback: '有什麼反饋嗎?', - rate: '打個分吧!', - helpUs: '帮助我们~', - userManual: '使用者手冊', - }, - en: { - dashboard: 'Dashboard', - data: 'My Data', - dataReport: 'Record', - siteAnalysis: 'Site Analysis', - dataClear: 'Memory Situation', - behavior: 'User Behavior', - habit: 'Habits', - limit: 'Daily Limit', - additional: 'Additional Features', - siteManage: 'Site Management', - whitelist: 'Whitelist', - mergeRule: 'Merge-site Rules', - other: 'Other Features', - option: 'Options', - feedback: 'Feedback Questionnaire', - rate: 'Rate It', - helpUs: 'Help Us', - userManual: 'User Manual', - }, - ja: { - dashboard: 'ダッシュボード', - data: '私のデータ', - dataReport: '報告する', - siteAnalysis: 'ウェブサイト分析', - dataClear: '記憶状況', - behavior: 'ユーザーの行動', - habit: '閲覧の習慣', - limit: '閲覧の制限', - additional: 'その他の機能', - siteManage: 'ウェブサイト管理', - whitelist: 'Webホワイトリスト', - mergeRule: 'ドメイン合併', - other: 'その他の機能', - option: '拡張設定', - feedback: 'フィードバックアンケート', - rate: 'それを評価', - helpUs: '協力する', - userManual: 'ユーザーマニュアル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/merge-rule-resource.json b/src/i18n/message/app/merge-rule-resource.json new file mode 100644 index 000000000..b1fe8d3ad --- /dev/null +++ b/src/i18n/message/app/merge-rule-resource.json @@ -0,0 +1,62 @@ +{ + "zh_CN": { + "removeConfirmMsg": "自定义合并规则 {origin} 将被移除", + "originPlaceholder": "原域名", + "mergedPlaceholder": "合并后域名", + "errorOrigin": "原域名格式错误", + "duplicateMsg": "合并规则已存在:{origin}", + "addConfirmMsg": "将为 {origin} 设置自定义合并规则", + "infoAlertTitle": "该页面可以配置子域名的合并规则", + "infoAlert0": "点击新增按钮,会弹出原域名和合并后域名的输入框,填写并保存规则", + "infoAlert1": "原域名可填具体的域名或者正则表达式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此确定哪些域名在合并时会使用该条规则", + "infoAlert2": "合并后域名可填具体的域名,或者填数字,或者不填", + "infoAlert3": "如果填数字,则表示合并后域名的级数。比如存在规则【 *.*.edu.cn >>> 3 】,那么 www.hust.edu.cn 将被合并至 hust.edu.cn", + "infoAlert4": "如果不填,则表示原域名不会被合并", + "infoAlert5": "如果没有命中任何规则,则默认会合并至 {psl} 的前一级" + }, + "zh_TW": { + "removeConfirmMsg": "自定義合並規則 {origin} 將被移除", + "originPlaceholder": "原網域", + "mergedPlaceholder": "合並後網域", + "errorOrigin": "原網域格式錯誤", + "duplicateMsg": "合並規則已存在:{origin}", + "addConfirmMsg": "將爲 {origin} 設置自定義合並規則", + "infoAlertTitle": "該頁麵可以配置子網域的合並規則", + "infoAlert0": "點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則", + "infoAlert1": "原網域可填具體的網域或者正則表達式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則", + "infoAlert2": "合並後網域可填具體的網域,或者填數字,或者不填", + "infoAlert3": "如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn", + "infoAlert4": "如果不填,則表示原網域不會被合並", + "infoAlert5": "如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級" + }, + "en": { + "removeConfirmMsg": "{origin} will be removed from customized merge rules.", + "originPlaceholder": "Original site", + "mergedPlaceholder": "Merged", + "errorOrigin": "The format of original site is invalid.", + "duplicateMsg": "The rule already exists: {origin}", + "addConfirmMsg": "Customized merge rules will be set for {origin}", + "infoAlertTitle": "You can set the merge rules when counting sites on this page", + "infoAlert0": "Click the [New One] button, the input boxes of the source site and the merge site will be displayed, fill in and save the rule", + "infoAlert1": "The original site can be filled with a specific site or regular expression, such as www.baidu.com, *.baidu.com, *.google.com.*, to determine which sites will match this rule while merging", + "infoAlert2": "The merged site can be filled with a specific site, a number or blank", + "infoAlert3": "A number means the level of merged site. For example, there is a rule '*.*.edu.cn >>> 3', then 'www.hust.edu.cn' will be merged to 'hust.edu.cn'", + "infoAlert4": "Blank means the original site will not be merged", + "infoAlert5": "If no rule is matched, it will default to the level before {psl}" + }, + "ja": { + "removeConfirmMsg": "カスタム マージ ルール {origin} は削除されます", + "originPlaceholder": "独自ドメイン名", + "mergedPlaceholder": "統計的ドメイン名", + "errorOrigin": "元のドメイン名の形式が間違っています", + "duplicateMsg": "ルールはすでに存在します:{origin}", + "addConfirmMsg": "カスタム マージ ルールが {origin} に設定されます", + "infoAlertTitle": "このページでは、サブドメインのマージ ルールを設定できます", + "infoAlert0": "[追加] ボタンをクリックすると、元のドメイン名と結合されたドメイン名の入力ボックスがポップアップし、ルールを入力して保存します。", + "infoAlert1": "元のドメイン名には、特定のドメイン名または正規表現 (www.baidu.com、*.baidu.com、*.google.com.* など) を入力できます。 マージ時にこのルールを使用するドメインを決定するには", + "infoAlert2": "統合されたドメイン名の後、特定のドメイン名を入力するか、番号を入力するか、空白のままにすることができます", + "infoAlert3": "数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。", + "infoAlert4": "記入しない場合は、元のドメイン名が統合されないことを意味します", + "infoAlert5": "一致するルールがない場合、デフォルトで {psl} より前のレベルになります" + } +} \ 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 544f0caa5..ab5969afe 100644 --- a/src/i18n/message/app/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './merge-rule-resource.json' + export type MergeRuleMessage = { removeConfirmMsg: string originPlaceholder: string @@ -21,67 +23,6 @@ export type MergeRuleMessage = { infoAlert5: string } -const _default: Messages = { - zh_CN: { - removeConfirmMsg: '自定义合并规则 {origin} 将被移除', - originPlaceholder: '原域名', - mergedPlaceholder: '合并后域名', - errorOrigin: '原域名格式错误', - duplicateMsg: '合并规则已存在:{origin}', - addConfirmMsg: '将为 {origin} 设置自定义合并规则', - infoAlertTitle: '该页面可以配置子域名的合并规则', - infoAlert0: '点击新增按钮,会弹出原域名和合并后域名的输入框,填写并保存规则', - infoAlert1: '原域名可填具体的域名或者正则表达式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此确定哪些域名在合并时会使用该条规则', - infoAlert2: '合并后域名可填具体的域名,或者填数字,或者不填', - infoAlert3: '如果填数字,则表示合并后域名的级数。比如存在规则【 *.*.edu.cn >>> 3 】,那么 www.hust.edu.cn 将被合并至 hust.edu.cn', - infoAlert4: '如果不填,则表示原域名不会被合并', - infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级', - }, - zh_TW: { - removeConfirmMsg: '自定義合並規則 {origin} 將被移除', - originPlaceholder: '原網域', - mergedPlaceholder: '合並後網域', - errorOrigin: '原網域格式錯誤', - duplicateMsg: '合並規則已存在:{origin}', - addConfirmMsg: '將爲 {origin} 設置自定義合並規則', - infoAlertTitle: '該頁麵可以配置子網域的合並規則', - infoAlert0: '點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則', - infoAlert1: '原網域可填具體的網域或者正則表達式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則', - infoAlert2: '合並後網域可填具體的網域,或者填數字,或者不填', - infoAlert3: '如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn', - infoAlert4: '如果不填,則表示原網域不會被合並', - infoAlert5: '如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級', - }, - en: { - removeConfirmMsg: '{origin} will be removed from customized merge rules.', - originPlaceholder: 'Origin site', - mergedPlaceholder: 'Merged', - errorOrigin: 'The format of origin site is invalid.', - duplicateMsg: 'The rule already exists: {origin}', - addConfirmMsg: 'Customized merge rules will be set for {origin}', - infoAlertTitle: 'You can set the merging rules when counting sites on this page', - infoAlert0: 'Click the [New One] button, the input boxes of the source site and the merge site will be displayed, fill in and save the rule', - infoAlert1: 'The origin site can be filled with a specific site or regular expression, such as www.baidu.com, *.baidu.com, *.google.com.*, to determine which sites will match this rule while merging', - infoAlert2: 'The merged site can be filled with a specific site, a number or blank', - infoAlert3: 'A number means the level of merged site. For example, there is a rule \'*.*.edu.cn >>> 3\', then \'www.hust.edu.cn\' will be merged to \'hust.edu.cn\'', - infoAlert4: 'Blank means the origin site will not be merged', - infoAlert5: 'If no rule is matched, it will default to the level before {psl}', - }, - ja: { - removeConfirmMsg: 'カスタム マージ ルール {origin} は削除されます', - originPlaceholder: '独自ドメイン名', - mergedPlaceholder: '統計的ドメイン名', - errorOrigin: '元のドメイン名の形式が間違っています', - duplicateMsg: 'ルールはすでに存在します:{origin}', - addConfirmMsg: 'カスタム マージ ルールが {origin} に設定されます', - infoAlertTitle: 'このページでは、サブドメインのマージ ルールを設定できます', - infoAlert0: '[追加] ボタンをクリックすると、元のドメイン名と結合されたドメイン名の入力ボックスがポップアップし、ルールを入力して保存します。', - infoAlert1: '元のドメイン名には、特定のドメイン名または正規表現 (www.baidu.com、*.baidu.com、*.google.com.* など) を入力できます。 マージ時にこのルールを使用するドメインを決定するには', - infoAlert2: '統合されたドメイン名の後、特定のドメイン名を入力するか、番号を入力するか、空白のままにすることができます', - infoAlert3: '数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。', - infoAlert4: '記入しない場合は、元のドメイン名が統合されないことを意味します', - infoAlert5: '一致するルールがない場合、デフォルトで {psl} より前のレベルになります', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/operation-resource.json b/src/i18n/message/app/operation-resource.json new file mode 100644 index 000000000..0380f970e --- /dev/null +++ b/src/i18n/message/app/operation-resource.json @@ -0,0 +1,26 @@ +{ + "zh_CN": { + "confirmTitle": "操作确认", + "successMsg": "操作成功!", + "newOne": "新增", + "save": "保存" + }, + "zh_TW": { + "confirmTitle": "操作確認", + "successMsg": "操作成功!", + "newOne": "新增", + "save": "保存" + }, + "en": { + "confirmTitle": "Confirm", + "successMsg": "Successfully!", + "newOne": "New One", + "save": "Save" + }, + "ja": { + "confirmTitle": "動作確認", + "successMsg": "正常に動作しました!", + "newOne": "追加", + "save": "保存" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/operation.ts b/src/i18n/message/app/operation.ts index 7eea0c7d5..2a6788177 100644 --- a/src/i18n/message/app/operation.ts +++ b/src/i18n/message/app/operation.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './operation-resource.json' + export type OperationMessage = { confirmTitle: string successMsg: string @@ -12,31 +14,6 @@ export type OperationMessage = { newOne: string } -const _default: Messages = { - zh_CN: { - confirmTitle: '操作确认', - successMsg: '操作成功!', - newOne: '新增', - save: '保存', - }, - zh_TW: { - confirmTitle: '操作確認', - successMsg: '操作成功!', - newOne: '新增', - save: '保存', - }, - en: { - confirmTitle: 'Confirm', - successMsg: 'Successfully!', - newOne: 'New One', - save: 'Save', - }, - ja: { - confirmTitle: '動作確認', - successMsg: '正常に動作しました!', - newOne: '追加', - save: '保存', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json new file mode 100644 index 000000000..66a661ea3 --- /dev/null +++ b/src/i18n/message/app/option-resource.json @@ -0,0 +1,347 @@ +{ + "zh_CN": { + "yes": "是", + "no": "否", + "popup": { + "title": "弹窗页", + "max": "只显示前 {input} 条数据,剩下的条目合并显示", + "defaultMergeDomain": "{input} 打开时合并子域名", + "defaultDisplay": "打开时显示 {duration} {type}", + "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名", + "durationWidth": "80px", + "weekStart": "每周的第一天 {input}", + "weekStartAsNormal": "按照惯例" + }, + "appearance": { + "title": "外观", + "displayWhitelist": "{input} 是否在 {contextMenu} 里,显示 {whitelist} 相关功能", + "whitelistItem": "白名单", + "contextMenu": "浏览器的右键菜单", + "displayBadgeText": "{input} 是否在 {icon} 上,显示 {timeInfo}", + "icon": "扩展图标", + "badgeTextContent": "当前网站的今日浏览时长", + "locale": { + "label": "语言设置 {input}", + "default": "跟随浏览器", + "changeConfirm": "语言设置成功,请刷新页面!", + "reloadButton": "刷新" + }, + "printInConsole": { + "label": "{input} 是否在 {console} 里打印当前网站的 {info}", + "console": "浏览器的控制台", + "info": "今日访问信息" + }, + "darkMode": { + "label": "夜间模式 {input}", + "options": { + "default": "跟随浏览器", + "on": "始终开启", + "off": "始终关闭", + "timed": "定时开启" + } + }, + "limitFilterType": { + "label": "每日时限的背景风格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + } + }, + "statistics": { + "title": "统计", + "countWhenIdle": "{input} 是否统计 {idleTime} {info}", + "idleTime": "休眠时间", + "idleTimeInfo": "长时间不操作(比如全屏观看视频),浏览器会自动进入休眠状态", + "countLocalFiles": "{input} 是否统计使用浏览器 {localFileTime} {info}", + "localFileTime": "阅读本地文件的时间", + "localFilesInfo": "支持 PDF、图片、txt 以及 json 等格式", + "collectSiteName": "{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}", + "siteName": "网站的名称", + "siteNameUsage": "数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称" + }, + "backup": { + "title": "数据备份", + "type": "远端类型 {input}", + "client": "客户端标识 {input}", + "meta": { + "none": { + "label": "不开启备份", + "auth": "" + }, + "gist": { + "label": "GitHub Gist", + "auth": "Personal Access Token {info} {input}", + "authInfo": "需要创建一个至少包含 gist 权限的 token" + } + }, + "alert": "这是一项实验性功能,如果有任何问题请联系作者~ ({email})", + "test": "测试", + "lastTimeTip": "上次备份时间: {lastTime}", + "operation": "备份数据", + "auto": { + "label": "是否开启自动备份", + "interval": "每 {input} 分钟备份一次" + } + }, + "resetButton": "恢复默认", + "resetSuccess": "成功重置为默认值", + "defaultValue": "默认值: {default}" + }, + "zh_TW": { + "yes": "是", + "no": "否", + "popup": { + "title": "彈窗頁", + "max": "隻顯示前 {input} 條數據,剩下的條目合並顯示", + "defaultMergeDomain": "{input} 打開時合併子域名", + "defaultDisplay": "打開時顯示 {duration} {type}", + "displaySiteName": "{input} 顯示時是否使用 {siteName} 來代替域名", + "durationWidth": "80px", + "weekStart": "每週的第一天 {input}", + "weekStartAsNormal": "按照慣例" + }, + "appearance": { + "title": "外觀", + "displayWhitelist": "{input} 是否在 {contextMenu} 裡,顯示 {whitelist} 相關功能", + "whitelistItem": "白名單", + "contextMenu": "瀏覽器的右鍵菜單", + "displayBadgeText": "{input} 是否在 {icon} 上,顯示 {timeInfo}", + "icon": "擴充圖標", + "badgeTextContent": "當前網站的今日瀏覽時長", + "locale": { + "label": "語言設置 {input}", + "default": "跟隨瀏覽器", + "changeConfirm": "語言設置成功,請刷新頁麵!", + "reloadButton": "刷新" + }, + "printInConsole": { + "label": "{input} 是否在 {console} 裡打印當前網站的 {info}", + "console": "瀏覽器控制台", + "info": "今日拜訪信息" + }, + "darkMode": { + "label": "黑暗模式 {input}", + "options": { + "default": "跟隨瀏覽器", + "on": "始終開啟", + "off": "始終關閉", + "timed": "定時開啟" + } + }, + "limitFilterType": { + "label": "每日時限的背景風格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + } + }, + "statistics": { + "title": "統計", + "countWhenIdle": "{input} 是否統計 {idleTime} {info}", + "idleTime": "休眠時間", + "idleTimeInfo": "長時間不操作(比如全屏觀看視頻),瀏覽器會自動進入休眠狀態", + "countLocalFiles": "{input} 是否統計使用瀏覽器 {localFileTime} {info}", + "localFileTime": "閱讀本地文件的時間", + "localFilesInfo": "支持 PDF、圖片、txt 以及 json 等格式", + "collectSiteName": "{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}", + "siteName": "網站的名稱", + "siteNameUsage": "數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱" + }, + "backup": { + "title": "數據備份", + "type": "雲端類型 {input}", + "client": "客戶端標識 {input}", + "meta": { + "none": { + "label": "關閉備份" + }, + "gist": { + "label": "GitHub Gist", + "auth": "Personal Access Token {info} {input}", + "authInfo": "需要創建一個至少包含 gist 權限的 token" + } + }, + "alert": "這是一項實驗性功能,如果有任何問題請聯繫作者 ({email}) ~", + "test": "測試", + "operation": "備份數據", + "lastTimeTip": "上次備份時間: {lastTime}", + "auto": { + "label": "是否開啟自動備份", + "interval": "每 {input} 分鐘備份一次" + } + }, + "resetButton": "恢複默認", + "resetSuccess": "成功重置爲默認值", + "defaultValue": "默認值: {default}" + }, + "en": { + "yes": "Yes", + "no": "No", + "popup": { + "title": "Popup Page", + "max": "Show the first {input} data items", + "defaultMergeDomain": "{input} Whether to merge subdomains on open", + "defaultDisplay": "Show {duration} {type} on open", + "displaySiteName": "{input} Whether to display {siteName} instead of URL", + "durationWidth": "110px", + "weekStart": "The first day for each week {input}", + "weekStartAsNormal": "As Normal" + }, + "appearance": { + "title": "Appearance", + "displayWhitelist": "{input} Whether to display {whitelist} in {contextMenu}", + "whitelistItem": "whitelist related shortcuts", + "contextMenu": "the context menu", + "displayBadgeText": "{input} Whether to display {timeInfo} in {icon}", + "icon": "the icon of extension", + "badgeTextContent": "the browsing time of current website", + "locale": { + "label": "Language {input}", + "default": "Follow browser", + "changeConfirm": "The language has been changed successfully, please reload this page!", + "reloadButton": "Reload" + }, + "printInConsole": { + "label": "{input} Whether to print {info} in the {console}", + "console": "console", + "info": "the visit count of the current website today" + }, + "darkMode": { + "label": "Dark mode {input}", + "options": { + "default": "Follow browser", + "on": "Always on", + "off": "Always off", + "timed": "Timed on" + } + }, + "limitFilterType": { + "label": "Background style for daily time limit {input}", + "translucent": "Translucent", + "groundGlass": "Ground Glass" + } + }, + "statistics": { + "title": "Statistics", + "countWhenIdle": "{input} Whether to count {idleTime} {info}", + "idleTime": "idle time", + "idleTimeInfo": "If you do not operate for a long time (such as watching a video in full screen), the browser will automatically enter the idle state", + "countLocalFiles": "{input} Whether to count the time to {localFileTime} {info} in the browser", + "localFileTime": " read a local file ", + "localFilesInfo": "Supports files of types such as PDF, image, txt and json", + "collectSiteName": "{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage", + "siteName": " the site name ", + "siteNameUsage": "The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site." + }, + "backup": { + "title": "Data Backup", + "type": "Remote type {input}", + "client": "Client name {input}", + "meta": { + "none": { + "label": "Always off" + }, + "gist": { + "label": "GitHub Gist", + "auth": "Personal Access Token {info} {input}", + "authInfo": "One token with at least gist permission is required" + } + }, + "alert": "This is an experimental feature, if you have any questions please contact the author via {email}~", + "test": "Test", + "operation": "Backup", + "lastTimeTip": "Last backup time: {lastTime}", + "auto": { + "label": "Whether to enable automatic backup", + "interval": "and run every {input} minutes" + } + }, + "resetButton": "Reset", + "resetSuccess": "Reset to default successfully!", + "defaultValue": "Default: {default}" + }, + "ja": { + "yes": "はい", + "no": "いいえ", + "popup": { + "title": "ポップアップページ", + "max": "最初の {input} 個のデータのみを表示し、残りのエントリは結合されます", + "defaultMergeDomain": "{input} オープン時にサブドメインをマージ", + "defaultDisplay": "開くと {duration} {type} が表示されます", + "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか", + "durationWidth": "100px", + "weekStart": "週の最初の日 {input}", + "weekStartAsNormal": "いつものように" + }, + "appearance": { + "title": "外観", + "displayWhitelist": "{input} {contextMenu} に {whitelist} を表示するかどうか", + "whitelistItem": "ホワイトリスト機能", + "contextMenu": "コンテキストメニュー", + "displayBadgeText": "{input} {icon} に {timeInfo} を表示するかどうか", + "icon": "拡張機能のアイコン", + "badgeTextContent": "現在のウェブサイトの閲覧時間", + "locale": { + "label": "言語設定 {input}", + "default": "ブラウザと同じ", + "changeConfirm": "言語が正常に変更されました。このページをリロードしてください。", + "reloadButton": "リロード" + }, + "printInConsole": { + "label": "{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか", + "console": "コンソール", + "info": "今日の情報をご覧ください" + }, + "darkMode": { + "label": "ダークモード {input}", + "options": { + "default": "ブラウザと同じ", + "on": "常にオン", + "off": "常にオフ", + "timed": "時限スタート" + } + }, + "limitFilterType": { + "label": "毎日の時間制限の背景スタイル {input}", + "translucent": "半透明", + "groundGlass": "すりガラス" + } + }, + "statistics": { + "title": "統計", + "countWhenIdle": "{input} {idleTime} をカウントするかどうか {info}", + "idleTime": "アイドルタイム", + "idleTimeInfo": "長時間操作しない場合(フルスクリーンでビデオを見るなど)、ブラウザは自動的にアイドル状態になります", + "countLocalFiles": "{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか", + "localFileTime": " ローカルファイルの読み取り ", + "localFilesInfo": "PDF、画像、txt、jsonを含む", + "collectSiteName": "{input} ウェブサイトのホームページにアクセスしたときにウェブサイトの名前を自動的に収集するかどうか", + "siteName": "サイト名", + "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。" + }, + "backup": { + "title": "データバックアップ", + "type": "バックアップ方法 {input}", + "client": "クライアント名 {input}", + "meta": { + "none": { + "label": "バックアップを有効にしない" + }, + "gist": { + "label": "GitHub Gist", + "auth": "Personal Access Token {info} {input}", + "authInfo": "少なくとも gist 権限を持つトークンが 1 つ必要です" + } + }, + "alert": "これは実験的な機能です。質問がある場合は、作成者に連絡してください ({email})", + "test": "テスト", + "operation": "バックアップ", + "lastTimeTip": "前回のバックアップ時間: {lastTime}", + "auto": { + "label": "自動バックアップを有効にするかどうか", + "interval": " {input} 分ごとに実行" + } + }, + "resetButton": "リセット", + "resetSuccess": "デフォルトに正常にリセット", + "defaultValue": "デフォルト値:{default}" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 2c1ab322d..43273ebea 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './option-resource.json' + export type OptionMessage = { yes: string no: string @@ -84,352 +86,6 @@ export type OptionMessage = { defaultValue: string } -const _default: Messages = { - zh_CN: { - yes: '是', - no: '否', - popup: { - title: '弹窗页', - max: '只显示前 {input} 条数据,剩下的条目合并显示', - defaultMergeDomain: '{input} 打开时合并子域名', - defaultDisplay: '打开时显示 {duration} {type}', - displaySiteName: '{input} 显示时是否使用 {siteName} 来代替域名', - durationWidth: '80px', - weekStart: '每周的第一天 {input}', - weekStartAsNormal: '按照惯例', - }, - appearance: { - title: '外观', - displayWhitelist: '{input} 是否在 {contextMenu} 里,显示 {whitelist} 相关功能', - whitelistItem: '白名单', - contextMenu: '浏览器的右键菜单', - displayBadgeText: '{input} 是否在 {icon} 上,显示 {timeInfo}', - icon: '扩展图标', - badgeTextContent: '当前网站的今日浏览时长', - locale: { - label: '语言设置 {input}', - default: '跟随浏览器', - changeConfirm: '语言设置成功,请刷新页面!', - reloadButton: '刷新', - }, - printInConsole: { - label: '{input} 是否在 {console} 里打印当前网站的 {info}', - console: '浏览器的控制台', - info: '今日访问信息', - }, - darkMode: { - label: '夜间模式 {input}', - options: { - default: '跟随浏览器', - on: '始终开启', - off: '始终关闭', - timed: '定时开启', - }, - }, - limitFilterType: { - label: '每日时限的背景风格 {input}', - translucent: '半透明', - groundGlass: '毛玻璃', - }, - }, - statistics: { - title: '统计', - countWhenIdle: '{input} 是否统计 {idleTime} {info}', - idleTime: '休眠时间', - idleTimeInfo: '长时间不操作(比如全屏观看视频),浏览器会自动进入休眠状态', - countLocalFiles: '{input} 是否统计使用浏览器 {localFileTime} {info}', - localFileTime: '阅读本地文件的时间', - localFilesInfo: '支持 PDF、图片、txt 以及 json 等格式', - collectSiteName: '{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}', - siteName: '网站的名称', - siteNameUsage: '数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称', - }, - backup: { - title: '数据备份', - type: '远端类型 {input}', - client: '客户端标识 {input}', - meta: { - none: { - label: '不开启备份', - auth: '', - }, - gist: { - label: 'GitHub Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '需要创建一个至少包含 gist 权限的 token', - }, - }, - alert: '这是一项实验性功能,如果有任何问题请联系作者~ ({email})', - test: '测试', - lastTimeTip: '上次备份时间: {lastTime}', - operation: '备份数据', - auto: { - label: '是否开启自动备份', - interval: '每 {input} 分钟备份一次', - }, - }, - resetButton: '恢复默认', - resetSuccess: '成功重置为默认值', - defaultValue: '默认值: {default}', - }, - zh_TW: { - yes: '是', - no: '否', - popup: { - title: '彈窗頁', - max: '隻顯示前 {input} 條數據,剩下的條目合並顯示', - defaultMergeDomain: '{input} 打開時合併子域名', - defaultDisplay: '打開時顯示 {duration} {type}', - displaySiteName: '{input} 顯示時是否使用 {siteName} 來代替域名', - durationWidth: '80px', - weekStart: '每週的第一天 {input}', - weekStartAsNormal: '按照慣例', - }, - appearance: { - title: '外觀', - displayWhitelist: '{input} 是否在 {contextMenu} 裡,顯示 {whitelist} 相關功能', - whitelistItem: '白名單', - contextMenu: '瀏覽器的右鍵菜單', - displayBadgeText: '{input} 是否在 {icon} 上,顯示 {timeInfo}', - icon: '擴充圖標', - badgeTextContent: '當前網站的今日瀏覽時長', - locale: { - label: '語言設置 {input}', - default: '跟隨瀏覽器', - changeConfirm: '語言設置成功,請刷新頁麵!', - reloadButton: '刷新', - }, - printInConsole: { - label: '{input} 是否在 {console} 裡打印當前網站的 {info}', - console: '瀏覽器控制台', - info: '今日拜訪信息', - }, - darkMode: { - label: '黑暗模式 {input}', - options: { - default: '跟隨瀏覽器', - on: '始終開啟', - off: '始終關閉', - timed: '定時開啟', - }, - }, - limitFilterType: { - label: '每日時限的背景風格 {input}', - translucent: '半透明', - groundGlass: '毛玻璃', - }, - }, - statistics: { - title: '統計', - countWhenIdle: '{input} 是否統計 {idleTime} {info}', - idleTime: '休眠時間', - idleTimeInfo: '長時間不操作(比如全屏觀看視頻),瀏覽器會自動進入休眠狀態', - countLocalFiles: '{input} 是否統計使用瀏覽器 {localFileTime} {info}', - localFileTime: '閱讀本地文件的時間', - localFilesInfo: '支持 PDF、圖片、txt 以及 json 等格式', - collectSiteName: '{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}', - siteName: '網站的名稱', - siteNameUsage: '數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱', - }, - backup: { - title: '數據備份', - type: '雲端類型 {input}', - client: '客戶端標識 {input}', - meta: { - none: { - label: '關閉備份', - }, - gist: { - label: 'GitHub Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '需要創建一個至少包含 gist 權限的 token', - }, - }, - alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 ({email}) ~', - test: '測試', - operation: '備份數據', - lastTimeTip: '上次備份時間: {lastTime}', - auto: { - label: '是否開啟自動備份', - interval: '每 {input} 分鐘備份一次', - }, - }, - resetButton: '恢複默認', - resetSuccess: '成功重置爲默認值', - defaultValue: '默認值: {default}', - }, - en: { - yes: 'Yes', - no: 'No', - popup: { - title: 'Popup Page', - max: 'Show the first {input} data items', - defaultMergeDomain: '{input} Whether to merge subdomains on open', - defaultDisplay: 'Show {duration} {type} on open', - displaySiteName: '{input} Whether to display {siteName} instead of URL', - durationWidth: '110px', - weekStart: 'The first day for each week {input}', - weekStartAsNormal: 'As Normal', - }, - appearance: { - title: 'Appearance', - displayWhitelist: '{input} Whether to display {whitelist} in {contextMenu}', - whitelistItem: 'whitelist related shortcuts', - contextMenu: 'the context menu', - displayBadgeText: '{input} Whether to display {timeInfo} in {icon}', - icon: 'the icon of extension', - badgeTextContent: 'the browsing time of current website', - locale: { - label: 'Language {input}', - default: 'Follow browser', - changeConfirm: 'The language has been changed successfully, please reload this page!', - reloadButton: 'Reload', - }, - printInConsole: { - label: '{input} Whether to print {info} in the {console}', - console: 'console', - info: 'the visit count of the current website today', - }, - darkMode: { - label: 'Dark mode {input}', - options: { - default: 'Follow browser', - on: 'Always on', - off: 'Always off', - timed: 'Timed on', - }, - }, - limitFilterType: { - label: 'Background style for daily time limit {input}', - translucent: 'Translucent', - groundGlass: 'Ground Glass', - }, - }, - statistics: { - title: 'Statistics', - countWhenIdle: '{input} Whether to count {idleTime} {info}', - idleTime: 'idle time', - idleTimeInfo: 'If you do not operate for a long time (such as watching a video in full screen), the browser will automatically enter the idle state', - countLocalFiles: '{input} Whether to count the time to {localFileTime} {info} in the browser', - localFileTime: ' read a local file ', - localFilesInfo: 'Supports files of types such as PDF, image, txt and json', - collectSiteName: '{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage', - siteName: ' the site name ', - siteNameUsage: 'The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site.', - }, - backup: { - title: 'Data Backup', - type: 'Remote type {input}', - client: 'Client name {input}', - meta: { - none: { - label: 'Always off', - }, - gist: { - label: 'GitHub Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: 'One token with at least gist permission is required', - }, - }, - alert: 'This is an experimental feature, if you have any questions please contact the author via {email}~', - test: 'Test', - operation: 'Backup', - lastTimeTip: 'Last backup time: {lastTime}', - auto: { - label: 'Whether to enable automatic backup', - interval: 'and run every {input} minutes', - }, - }, - resetButton: 'Reset', - resetSuccess: 'Reset to default successfully!', - defaultValue: 'Default: {default}', - }, - ja: { - yes: 'はい', - no: 'いいえ', - popup: { - title: 'ポップアップページ', - max: '最初の {input} 個のデータのみを表示し、残りのエントリは結合されます', - defaultMergeDomain: '{input} オープン時にサブドメインをマージ', - defaultDisplay: '開くと {duration} {type} が表示されます', - displaySiteName: '{input} ホストの代わりに {siteName} を表示するかどうか', - durationWidth: '100px', - weekStart: '週の最初の日 {input}', - weekStartAsNormal: 'いつものように', - }, - appearance: { - title: '外観', - displayWhitelist: '{input} {contextMenu} に {whitelist} を表示するかどうか', - whitelistItem: 'ホワイトリスト機能', - contextMenu: 'コンテキストメニュー', - displayBadgeText: '{input} {icon} に {timeInfo} を表示するかどうか', - icon: '拡張機能のアイコン', - badgeTextContent: '現在のウェブサイトの閲覧時間', - locale: { - label: '言語設定 {input}', - default: 'ブラウザと同じ', - changeConfirm: '言語が正常に変更されました。このページをリロードしてください。', - reloadButton: 'リロード', - }, - printInConsole: { - label: '{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか', - console: 'コンソール', - info: '今日の情報をご覧ください', - }, - darkMode: { - label: 'ダークモード {input}', - options: { - default: 'ブラウザと同じ', - on: '常にオン', - off: '常にオフ', - timed: '時限スタート', - }, - }, - limitFilterType: { - label: '毎日の時間制限の背景スタイル {input}', - translucent: '半透明', - groundGlass: 'すりガラス', - }, - }, - statistics: { - title: '統計', - countWhenIdle: '{input} {idleTime} をカウントするかどうか {info}', - idleTime: 'アイドルタイム', - idleTimeInfo: '長時間操作しない場合(フルスクリーンでビデオを見るなど)、ブラウザは自動的にアイドル状態になります', - countLocalFiles: '{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか', - localFileTime: ' ローカルファイルの読み取り ', - localFilesInfo: 'PDF、画像、txt、jsonを含む', - collectSiteName: '{input} ウェブサイトのホームページにアクセスしたときにウェブサイトの名前を自動的に収集するかどうか', - siteName: 'サイト名', - siteNameUsage: 'データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。', - }, - backup: { - title: 'データバックアップ', - type: 'バックアップ方法 {input}', - client: 'クライアント名 {input}', - meta: { - none: { - label: 'バックアップを有効にしない', - }, - gist: { - label: 'GitHub Gist', - auth: 'Personal Access Token {info} {input}', - authInfo: '少なくとも gist 権限を持つトークンが 1 つ必要です', - }, - }, - alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください ({email})', - test: 'テスト', - operation: 'バックアップ', - lastTimeTip: '前回のバックアップ時間: {lastTime}', - auto: { - label: '自動バックアップを有効にするかどうか', - interval: ' {input} 分ごとに実行', - }, - }, - resetButton: 'リセット', - resetSuccess: 'デフォルトに正常にリセット', - defaultValue: 'デフォルト値:{default}', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json new file mode 100644 index 000000000..bd79f4531 --- /dev/null +++ b/src/i18n/message/app/report-resource.json @@ -0,0 +1,130 @@ +{ + "zh_CN": { + "startDate": "开始日期", + "endDate": "结束日期", + "lastWeek": "最近一周", + "last30Days": "最近 30 天", + "today": "今天", + "yesterday": "昨天", + "mergeDate": "合并日期", + "mergeDomain": "合并子域名", + "hostPlaceholder": "请输入域名,然后回车", + "exportFileName": "我的上网时间", + "added2Whitelist": "成功加入白名单", + "removeFromWhitelist": "成功从白名单移除", + "batchDelete": { + "buttonText": "批量删除", + "noSelectedMsg": "请先在表格中勾选需要删除的行", + "confirmMsg": "{example} 等网站在 {date} 的 {count} 条记录将会被删除!", + "confirmMsgAll": "{example} 等网站的 {count} 条记录将会被删除!", + "confirmMsgRange": "{example} 等网站在 {start} 至 {end} 之间的 {count} 条记录将会被删除!", + "successMsg": "成功批量删除" + }, + "remoteReading": { + "on": "正在查询远端备份数据", + "off": "单击以开启远端备份数据查询功能", + "table": { + "client": "客户端", + "localData": "本地数据", + "value": "对应数值", + "percentage": "百分比" + } + } + }, + "zh_TW": { + "startDate": "開始日期", + "endDate": "結束日期", + "lastWeek": "最近一週", + "last30Days": "最近 30 天", + "today": "今天", + "yesterday": "昨天", + "mergeDate": "合並日期", + "mergeDomain": "合並子域名", + "hostPlaceholder": "請輸入域名,然後回車", + "exportFileName": "我的上網時間", + "added2Whitelist": "成功加入白名單", + "removeFromWhitelist": "成功從白名單移除", + "batchDelete": { + "buttonText": "批量刪除", + "noSelectedMsg": "請先在表格中勾選需要刪除的行", + "confirmMsg": "{example} 等網站在 {date} 的 {count} 條記錄將會被刪除!", + "confirmMsgAll": "{example} 等網站的 {count} 條記錄將會被刪除!", + "confirmMsgRange": "{example} 等網站在 {start} 至 {end} 之間的 {count} 條記錄將會被刪除!", + "successMsg": "成功批量刪除" + }, + "remoteReading": { + "on": "正在查詢遠端備份數據", + "off": "單擊以開啟遠端備份數據查詢功能", + "table": { + "client": "客户端", + "localData": "本地數據", + "value": "對應數值", + "percentage": "百分比" + } + } + }, + "en": { + "startDate": "Start date", + "endDate": "End date", + "lastWeek": "Last week", + "last30Days": "Last 30 days", + "today": "Today", + "yesterday": "Yesterday", + "mergeDate": "Merge date", + "mergeDomain": "Merge URL", + "hostPlaceholder": "Partial URL, then enter", + "exportFileName": "Timer_Data", + "added2Whitelist": "Added into the whitelist", + "removeFromWhitelist": "Removed from the whitelist", + "batchDelete": { + "buttonText": "Batch delete", + "noSelectedMsg": "Please select the row you want to delete in the table first", + "confirmMsg": "{count} records for sites like {example} on {date} will be deleted!", + "confirmMsgAll": "{count} records for sites like {example} will be deleted!", + "confirmMsgRange": "{count} records for sites like {example} between {start} and {end} will be deleted!", + "successMsg": "Batch delete successfully" + }, + "remoteReading": { + "on": "Reading remote backuped data", + "off": "Click to read remote backuped data", + "table": { + "client": "Client's Name", + "localData": "Local Data", + "value": "Value", + "percentage": "Percentage" + } + } + }, + "ja": { + "startDate": "開始日", + "endDate": "終了日", + "lastWeek": "先週", + "last30Days": "過去 30 日間", + "today": "今日", + "yesterday": "昨日", + "mergeDate": "マージ日", + "mergeDomain": "URLをマージ", + "hostPlaceholder": "URL を入力してください", + "exportFileName": "私のウェブ時間データ", + "added2Whitelist": "ホワイトリストに正常に追加されました", + "removeFromWhitelist": "ホワイトリストから正常に削除されました", + "batchDelete": { + "buttonText": "バッチ削除", + "noSelectedMsg": "最初にテーブルで削除する行にチェックマークを付けてください", + "confirmMsg": "{date} の {example} のようなサイトの {count} レコードは削除されます!", + "confirmMsgAll": "{example} のようなサイトの {count} レコードは削除されます!", + "confirmMsgRange": "{start} と {end} の間の {example} のようなサイトの {count} レコードが削除されます!", + "successMsg": "バッチ削除に成功" + }, + "remoteReading": { + "on": "リモート バックアップ データのクエリ", + "off": "クリックして、リモート バックアップ データのクエリ機能を有効にします", + "table": { + "client": "クライアントの名前", + "localData": "ローカル データ", + "value": "対応する値", + "percentage": "パーセンテージ" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/report.ts b/src/i18n/message/app/report.ts index cb8d79cfa..aafc0d905 100644 --- a/src/i18n/message/app/report.ts +++ b/src/i18n/message/app/report.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './report-resource.json' + export type ReportMessage = { startDate: string endDate: string @@ -38,135 +40,6 @@ export type ReportMessage = { } } -const _default: Messages = { - zh_CN: { - startDate: '开始日期', - endDate: '结束日期', - lastWeek: '最近一周', - last30Days: '最近 30 天', - today: '今天', - yesterday: '昨天', - mergeDate: '合并日期', - mergeDomain: '合并子域名', - hostPlaceholder: '请输入域名,然后回车', - exportFileName: '我的上网时间', - added2Whitelist: '成功加入白名单', - removeFromWhitelist: '成功从白名单移除', - batchDelete: { - buttonText: '批量删除', - noSelectedMsg: '请先在表格中勾选需要删除的行', - confirmMsg: '{example} 等网站在 {date} 的 {count} 条记录将会被删除!', - confirmMsgAll: '{example} 等网站的 {count} 条记录将会被删除!', - confirmMsgRange: '{example} 等网站在 {start} 至 {end} 之间的 {count} 条记录将会被删除!', - successMsg: '成功批量删除', - }, - remoteReading: { - on: '正在查询远端备份数据', - off: '单击以开启远端备份数据查询功能', - table: { - client: '客户端', - localData: '本地数据', - value: '对应数值', - percentage: '百分比', - }, - }, - }, - zh_TW: { - startDate: '開始日期', - endDate: '結束日期', - lastWeek: '最近一週', - last30Days: '最近 30 天', - today: '今天', - yesterday: '昨天', - mergeDate: '合並日期', - mergeDomain: '合並子域名', - hostPlaceholder: '請輸入域名,然後回車', - exportFileName: '我的上網時間', - added2Whitelist: '成功加入白名單', - removeFromWhitelist: '成功從白名單移除', - batchDelete: { - buttonText: '批量刪除', - noSelectedMsg: '請先在表格中勾選需要刪除的行', - confirmMsg: '{example} 等網站在 {date} 的 {count} 條記錄將會被刪除!', - confirmMsgAll: '{example} 等網站的 {count} 條記錄將會被刪除!', - confirmMsgRange: '{example} 等網站在 {start} 至 {end} 之間的 {count} 條記錄將會被刪除!', - successMsg: '成功批量刪除', - }, - remoteReading: { - on: '正在查詢遠端備份數據', - off: '單擊以開啟遠端備份數據查詢功能', - table: { - client: '客户端', - localData: '本地數據', - value: '對應數值', - percentage: '百分比', - }, - }, - }, - en: { - startDate: 'Start date', - endDate: 'End date', - lastWeek: 'Last week', - last30Days: 'Last 30 days', - today: 'Today', - yesterday: 'Yesterday', - mergeDate: 'Merge date', - mergeDomain: 'Merge URL', - hostPlaceholder: 'Partial URL, then enter', - exportFileName: 'Timer_Data', - added2Whitelist: 'Added into the whitelist', - removeFromWhitelist: 'Removed from the whitelist', - batchDelete: { - buttonText: 'Batch delete', - noSelectedMsg: 'Please select the row you want to delete in the table first', - confirmMsg: '{count} records for sites like {example} on {date} will be deleted!', - confirmMsgAll: '{count} records for sites like {example} will be deleted!', - confirmMsgRange: '{count} records for sites like {example} between {start} and {end} will be deleted!', - successMsg: 'Batch delete successfully', - }, - remoteReading: { - on: 'Reading remote backuped data', - off: 'Click to read remote backuped data', - table: { - client: 'Client\'s Name', - localData: 'Local Data', - value: 'Value', - percentage: 'Percentage', - }, - }, - }, - ja: { - startDate: '開始日', - endDate: '終了日', - lastWeek: '先週', - last30Days: '過去 30 日間', - today: '今日', - yesterday: '昨日', - mergeDate: 'マージ日', - mergeDomain: 'URLをマージ', - hostPlaceholder: 'URL を入力してください', - exportFileName: '私のウェブ時間データ', - added2Whitelist: 'ホワイトリストに正常に追加されました', - removeFromWhitelist: 'ホワイトリストから正常に削除されました', - batchDelete: { - buttonText: 'バッチ削除', - noSelectedMsg: '最初にテーブルで削除する行にチェックマークを付けてください', - confirmMsg: '{date} の {example} のようなサイトの {count} レコードは削除されます!', - confirmMsgAll: '{example} のようなサイトの {count} レコードは削除されます!', - confirmMsgRange: '{start} と {end} の間の {example} のようなサイトの {count} レコードが削除されます!', - successMsg: 'バッチ削除に成功', - }, - remoteReading: { - on: 'リモート バックアップ データのクエリ', - off: 'クリックして、リモート バックアップ データのクエリ機能を有効にします', - table: { - client: 'クライアントの名前', - localData: 'ローカル データ', - value: '対応する値', - percentage: 'パーセンテージ', - }, - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json new file mode 100644 index 000000000..41ca0b8c1 --- /dev/null +++ b/src/i18n/message/app/site-manage-resource.json @@ -0,0 +1,193 @@ +{ + "zh_CN": { + "hostPlaceholder": "请输入域名,然后回车", + "aliasPlaceholder": "请输入网站名,然后回车", + "onlyDetected": "只看自动抓取", + "deleteConfirmMsg": "{host} 的名称设置将会被删除", + "column": { + "host": "网站域名", + "type": "网站类型", + "alias": "网站名称", + "aliasInfo": "网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名", + "source": "名称来源", + "icon": "网站图标" + }, + "type": { + "normal": { + "name": "普通站点", + "info": "按域名的维度统计" + }, + "merged": { + "name": "合并站点", + "info": "将多个相关的域名合并统计,合并规则可以自定义" + }, + "virtual": { + "name": "自定义站点", + "info": "统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点" + } + }, + "source": { + "user": "手动设置", + "detected": "自动抓取" + }, + "button": { + "add": "新增", + "delete": "删除", + "save": "保存" + }, + "form": { + "emptyAlias": "请输入网站名称", + "emptyHost": "请输入网站域名" + }, + "msg": { + "hostExistWarn": "{host} 已经存在", + "saved": "已保存", + "existedTag": "已存在", + "mergedTag": "合并", + "virtualTag": "自定义" + } + }, + "zh_TW": { + "hostPlaceholder": "請輸入網域,然後回車", + "aliasPlaceholder": "請輸入網站名,然後回車", + "onlyDetected": "隻看自動抓取", + "deleteConfirmMsg": "{host} 的名稱設置將會被刪除", + "column": { + "host": "網站域名", + "type": "網站類型", + "alias": "網站名稱", + "aliasInfo": "網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域", + "source": "名稱來源", + "icon": "網站圖標" + }, + "source": { + "user": "手動設置", + "detected": "自動抓取" + }, + "type": { + "normal": { + "name": "普通站點", + "info": "按域名的維度統計" + }, + "merged": { + "name": "合併站點", + "info": "將多個相關的域名合併統計,合併規則可以自定義" + }, + "virtual": { + "name": "自定義站點", + "info": "統計 Ant Pattern 格式的任意 URL,可以在右上角新增自定義站點" + } + }, + "button": { + "add": "新增", + "delete": "刪除", + "save": "保存" + }, + "form": { + "emptyAlias": "請輸入網站名稱", + "emptyHost": "請輸入網站域名" + }, + "msg": { + "hostExistWarn": "{host} 已經存在", + "saved": "已保存", + "existedTag": "已存在", + "mergedTag": "合並" + } + }, + "en": { + "hostPlaceholder": "Partial URL, then enter", + "aliasPlaceholder": "Partial name, then enter", + "onlyDetected": "Only detected", + "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": "Name Source", + "icon": "Icon" + }, + "type": { + "normal": { + "name": "normal", + "info": "statistics by domain name" + }, + "merged": { + "name": "merged", + "info": "merge statistics of multiple related domain names, and the merge rules can be customized" + }, + "virtual": { + "name": "virtual", + "info": "count any URL in Ant Pattern format, you can add a custom site in the upper right corner" + } + }, + "source": { + "user": "user-maintained", + "detected": "auto-detected" + }, + "button": { + "add": "New", + "delete": "Delete", + "save": "Save" + }, + "form": { + "emptyAlias": "Please enter site name", + "emptyHost": "Please enter site URL" + }, + "msg": { + "hostExistWarn": "{host} exists", + "saved": "Saved", + "existedTag": "EXISTED", + "mergedTag": "MERGED", + "virtualTag": "VIRTUAL" + } + }, + "ja": { + "hostPlaceholder": "ドメイン名で検索", + "aliasPlaceholder": "サイト名で検索", + "onlyDetected": "検出されただけ", + "deleteConfirmMsg": "{host} の名前が削除されます", + "column": { + "host": "サイトのURL", + "alias": "サイト名", + "aliasInfo": "サイト名はレコードページとポップアップページに表示されます", + "source": "ソース", + "type": "サイト種別", + "icon": "Icon" + }, + "source": { + "user": "手动输入", + "detected": "システム検出" + }, + "button": { + "add": "追加", + "delete": "削除", + "save": "保存" + }, + "form": { + "emptyAlias": "サイト名を入力してください", + "emptyHost": "ドメイン名を入力してください" + }, + "msg": { + "hostExistWarn": "{host} が存在します", + "saved": "保存しました", + "existedTag": "既存", + "mergedTag": "合并", + "virtualTag": "バーチャル" + }, + "type": { + "normal": { + "name": "普通", + "info": "ドメイン名による統計" + }, + "merged": { + "name": "合并", + "info": "複数の関連するドメイン名のマージ統計をカスタマイズできます" + }, + "virtual": { + "name": "バーチャル", + "info": "Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます" + } + } + } +} \ No newline at end of file diff --git a/src/i18n/message/app/site-manage.ts b/src/i18n/message/app/site-manage.ts index 51488b5ab..dc39f02a7 100644 --- a/src/i18n/message/app/site-manage.ts +++ b/src/i18n/message/app/site-manage.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './site-manage-resource.json' + export type SiteManageMessage = { hostPlaceholder: string aliasPlaceholder: string @@ -41,198 +43,6 @@ export type SiteManageMessage = { } } -const _default: Messages = { - zh_CN: { - hostPlaceholder: '请输入域名,然后回车', - aliasPlaceholder: '请输入网站名,然后回车', - onlyDetected: '只看自动抓取', - deleteConfirmMsg: '{host} 的名称设置将会被删除', - column: { - host: '网站域名', - type: '网站类型', - alias: '网站名称', - aliasInfo: '网站名称会在报表以及今日数据(需要在扩展选项里设置)里展示,方便您快速识别域名', - source: '名称来源', - icon: '网站图标', - }, - type: { - normal: { - name: '普通站点', - info: '按域名的维度统计', - }, - merged: { - name: '合并站点', - info: '将多个相关的域名合并统计,合并规则可以自定义', - }, - virtual: { - name: '自定义站点', - info: '统计 Ant Pattern 格式的任意 URL,可以在右上角新增自定义站点', - }, - }, - source: { - user: '手动设置', - detected: '自动抓取', - }, - button: { - add: '新增', - delete: '删除', - save: '保存', - }, - form: { - emptyAlias: '请输入网站名称', - emptyHost: '请输入网站域名', - }, - msg: { - hostExistWarn: '{host} 已经存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合并', - virtualTag: '自定义', - }, - }, - zh_TW: { - hostPlaceholder: '請輸入網域,然後回車', - aliasPlaceholder: '請輸入網站名,然後回車', - onlyDetected: '隻看自動抓取', - deleteConfirmMsg: '{host} 的名稱設置將會被刪除', - column: { - host: '網站域名', - type: '網站類型', - alias: '網站名稱', - aliasInfo: '網站名稱會在報表以及今日數據(需要在擴充選項裡設置)裡展示,方便您快速識別網域', - source: '名稱來源', - icon: '網站圖標', - }, - source: { - user: '手動設置', - detected: '自動抓取', - }, - type: { - normal: { - name: '普通站點', - info: '按域名的維度統計', - }, - merged: { - name: '合併站點', - info: '將多個相關的域名合併統計,合併規則可以自定義', - }, - virtual: { - name: '自定義站點', - info: '統計 Ant Pattern 格式的任意 URL,可以在右上角新增自定義站點', - }, - }, - button: { - add: '新增', - delete: '刪除', - save: '保存', - }, - form: { - emptyAlias: '請輸入網站名稱', - emptyHost: '請輸入網站域名', - }, - msg: { - hostExistWarn: '{host} 已經存在', - saved: '已保存', - existedTag: '已存在', - mergedTag: '合並', - }, - }, - en: { - hostPlaceholder: 'Partial URL, then enter', - aliasPlaceholder: 'Partial name, then enter', - onlyDetected: 'Only detected', - 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: 'Name Source', - icon: 'Icon', - }, - type: { - normal: { - name: 'normal', - info: 'statistics by domain name', - }, - merged: { - name: 'merged', - info: 'merge statistics of multiple related domain names, and the merge rules can be customized', - }, - virtual: { - name: 'virtual', - info: 'count any URL in Ant Pattern format, you can add a custom site in the upper right corner', - }, - }, - source: { - user: 'user-maintained', - detected: 'auto-detected', - }, - button: { - add: 'New', - delete: 'Delete', - save: 'Save', - }, - form: { - emptyAlias: 'Please enter site name', - emptyHost: 'Please enter site URL', - }, - msg: { - hostExistWarn: '{host} exists', - saved: 'Saved', - existedTag: 'EXISTED', - mergedTag: 'MERGED', - virtualTag: 'VIRTUAL', - }, - }, - ja: { - hostPlaceholder: 'ドメイン名で検索', - aliasPlaceholder: 'サイト名で検索', - onlyDetected: '検出されただけ', - deleteConfirmMsg: '{host} の名前が削除されます', - column: { - host: 'サイトのURL', - alias: 'サイト名', - aliasInfo: 'サイト名はレコードページとポップアップページに表示されます', - source: 'ソース', - type: 'サイト種別', - icon: 'Icon', - }, - source: { - user: '手动输入', - detected: 'システム検出', - }, - button: { - add: '追加', - delete: '削除', - save: '保存', - }, - form: { - emptyAlias: 'サイト名を入力してください', - emptyHost: 'ドメイン名を入力してください', - }, - msg: { - hostExistWarn: '{host} が存在します', - saved: '保存しました', - existedTag: '既存', - mergedTag: '合并', - virtualTag: 'バーチャル', - }, - type: { - normal: { - name: '普通', - info: 'ドメイン名による統計', - }, - merged: { - name: '合并', - info: '複数の関連するドメイン名のマージ統計をカスタマイズできます', - }, - virtual: { - name: 'バーチャル', - info: 'Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます', - }, - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/app/time-format-resource.json b/src/i18n/message/app/time-format-resource.json new file mode 100644 index 000000000..a244f2cba --- /dev/null +++ b/src/i18n/message/app/time-format-resource.json @@ -0,0 +1,26 @@ +{ + "zh_CN": { + "default": "默认时间格式", + "hour": "按小时显示", + "minute": "按分钟显示", + "second": "按秒显示" + }, + "zh_TW": { + "default": "默認時間格式", + "hour": "按小時顯示", + "minute": "按分鐘顯示", + "second": "按秒顯示" + }, + "en": { + "default": "Default time format", + "hour": "Display in hours", + "minute": "Display in minutes", + "second": "Display in seconds" + }, + "ja": { + "default": "デフォルトの時間形式", + "hour": "時間単位で表示", + "minute": "分単位で表示", + "second": "秒単位で表示" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/time-format.ts b/src/i18n/message/app/time-format.ts index 25055d4e9..9a8c456e7 100644 --- a/src/i18n/message/app/time-format.ts +++ b/src/i18n/message/app/time-format.ts @@ -5,33 +5,10 @@ * https://opensource.org/licenses/MIT */ +import resource from './time-format-resource.json' + export type TimeFormatMessage = { [key in timer.app.TimeFormat]: string } -const _default: Messages = { - zh_CN: { - default: '默认时间格式', - hour: '按小时显示', - minute: '按分钟显示', - second: '按秒显示', - }, - zh_TW: { - default: '默認時間格式', - hour: '按小時顯示', - minute: '按分鐘顯示', - second: '按秒顯示', - }, - en: { - default: 'Default time format', - hour: 'Display in hours', - minute: 'Display in minutes', - second: 'Display in seconds', - }, - ja: { - default: 'デフォルトの時間形式', - hour: '時間単位で表示', - minute: '分単位で表示', - second: '秒単位で表示', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/app/whitelist-resource.json b/src/i18n/message/app/whitelist-resource.json new file mode 100644 index 000000000..fe9c424ab --- /dev/null +++ b/src/i18n/message/app/whitelist-resource.json @@ -0,0 +1,42 @@ +{ + "zh_CN": { + "addConfirmMsg": "{url} 加入白名单后,将不再统计该网站的数据", + "removeConfirmMsg": "{url} 将从白名单中移除", + "duplicateMsg": "已存在白名单中", + "infoAlertTitle": "你可以在这里配置网站白名单", + "infoAlert0": "白名单内网站的上网时长和打开次数不会被统计", + "infoAlert1": "白名单内网站的上网时间也不会被限制", + "placeholder": "域名", + "errorInput": "域名格式错误" + }, + "zh_TW": { + "addConfirmMsg": "{url} 加入白名單後,將不再統計該網站的數據", + "removeConfirmMsg": "{url} 將從白名單中移除", + "duplicateMsg": "已存在白名單中", + "infoAlertTitle": "你可以在這裡配置網站白名單", + "infoAlert0": "白名單內網站的上網時長和打開次數不會被統計", + "infoAlert1": "白名單內網站的上網時間也不會被限製", + "placeholder": "網域", + "errorInput": "網域格式錯誤" + }, + "en": { + "addConfirmMsg": "{url} won't be counted after added into the whitelist any more.", + "removeConfirmMsg": "{url} will be removed from the whitelist.", + "duplicateMsg": "Duplicated", + "infoAlertTitle": "You can set a whitelist of sites in this page", + "infoAlert0": "Whitelisted sites will not be counted", + "infoAlert1": "Whitelisted sites will not be restricted", + "placeholder": "Site URL", + "errorInput": "Invalid site URL" + }, + "ja": { + "addConfirmMsg": "{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。", + "removeConfirmMsg": "{url} はホワイトリストから削除されます", + "duplicateMsg": "繰り返される", + "infoAlertTitle": "このページでサイトのホワイトリストを設定できます", + "infoAlert0": "ホワイトリストのサイトはカウントされません。", + "infoAlert1": "ホワイトリストのサイトは制限されません。", + "placeholder": "URL", + "errorInput": "無効なURL" + } +} \ No newline at end of file diff --git a/src/i18n/message/app/whitelist.ts b/src/i18n/message/app/whitelist.ts index 255f5091b..035daeff6 100644 --- a/src/i18n/message/app/whitelist.ts +++ b/src/i18n/message/app/whitelist.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './whitelist-resource.json' + export type WhitelistMessage = { addConfirmMsg: string removeConfirmMsg: string @@ -16,47 +18,6 @@ export type WhitelistMessage = { errorInput: string } -const _default: Messages = { - zh_CN: { - addConfirmMsg: '{url} 加入白名单后,将不再统计该网站的数据', - removeConfirmMsg: '{url} 将从白名单中移除', - duplicateMsg: '已存在白名单中', - infoAlertTitle: '你可以在这里配置网站白名单', - infoAlert0: '白名单内网站的上网时长和打开次数不会被统计', - infoAlert1: '白名单内网站的上网时间也不会被限制', - placeholder: '域名', - errorInput: '域名格式错误', - }, - zh_TW: { - addConfirmMsg: '{url} 加入白名單後,將不再統計該網站的數據', - removeConfirmMsg: '{url} 將從白名單中移除', - duplicateMsg: '已存在白名單中', - infoAlertTitle: '你可以在這裡配置網站白名單', - infoAlert0: '白名單內網站的上網時長和打開次數不會被統計', - infoAlert1: '白名單內網站的上網時間也不會被限製', - placeholder: '網域', - errorInput: '網域格式錯誤', - }, - en: { - addConfirmMsg: '{url} won\'t be counted after added into the whitelist any more.', - removeConfirmMsg: '{url} will be removed from the whitelist.', - duplicateMsg: 'Duplicated', - infoAlertTitle: 'You can set a whitelist of sites in this page', - infoAlert0: 'Whitelisted sites will not be counted', - infoAlert1: 'Whitelisted sites will not be restricted', - placeholder: 'Site URL', - errorInput: 'Invalid site URL', - }, - ja: { - addConfirmMsg: '{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。', - removeConfirmMsg: '{url} はホワイトリストから削除されます', - duplicateMsg: '繰り返される', - infoAlertTitle: 'このページでサイトのホワイトリストを設定できます', - infoAlert0: 'ホワイトリストのサイトはカウントされません。', - infoAlert1: 'ホワイトリストのサイトは制限されません。', - placeholder: 'URL', - errorInput: '無効なURL', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/base-resource.json b/src/i18n/message/common/base-resource.json new file mode 100644 index 000000000..7f8168a20 --- /dev/null +++ b/src/i18n/message/common/base-resource.json @@ -0,0 +1,22 @@ +{ + "en": { + "currentVersion": "Version: {version}", + "allFunction": "All Functions", + "guidePage": "User Manual" + }, + "zh_CN": { + "currentVersion": "版本: v{version}", + "allFunction": "所有功能", + "guidePage": "用户手册" + }, + "zh_TW": { + "currentVersion": "版本: v{version}", + "allFunction": "所有功能", + "guidePage": "使用者手冊" + }, + "ja": { + "currentVersion": "バージョン: v{version}", + "allFunction": "すべての機能", + "guidePage": "ユーザーマニュアル" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/base.ts b/src/i18n/message/common/base.ts index 56d9331fe..4bc7c881e 100644 --- a/src/i18n/message/common/base.ts +++ b/src/i18n/message/common/base.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './base-resource.json' + export type BaseMessage = { currentVersion: string allFunction: string @@ -14,27 +16,6 @@ export type BaseMessage = { /** * Use for chrome */ -const _default: Messages = { - en: { - currentVersion: 'Version: {version}', - allFunction: 'All Functions', - guidePage: 'User Manual', - }, - zh_CN: { - currentVersion: '版本: v{version}', - allFunction: '所有功能', - guidePage: '用户手册', - }, - zh_TW: { - currentVersion: '版本: v{version}', - allFunction: '所有功能', - guidePage: '使用者手冊', - }, - ja: { - currentVersion: 'バージョン: v{version}', - allFunction: 'すべての機能', - guidePage: 'ユーザーマニュアル', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json new file mode 100644 index 000000000..b79ebc8cd --- /dev/null +++ b/src/i18n/message/common/calendar-resource.json @@ -0,0 +1,26 @@ +{ + "zh_CN": { + "weekDays": "星期一|星期二|星期三|星期四|星期五|星期六|星期天", + "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + }, + "zh_TW": { + "weekDays": "禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天", + "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + }, + "en": { + "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", + "dateFormat": "{m}/{d}/{y}", + "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}" + }, + "ja": { + "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", + "dateFormat": "{y}/{m}/{d}", + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index 46de6d195..f01fa91a8 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './calendar-resource.json' + export type CalendarMessage = { weekDays: string months: string @@ -12,31 +14,6 @@ export type CalendarMessage = { timeFormat: string } -const _default: Messages = { - zh_CN: { - weekDays: '星期一|星期二|星期三|星期四|星期五|星期六|星期天', - months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, - zh_TW: { - weekDays: '禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天', - months: '一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, - en: { - weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', - months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{m}/{d}/{y}', - timeFormat: '{m}/{d}/{y} {h}:{i}:{s}', - }, - ja: { - weekDays: 'Mon|Tue|Wed|Thu|Fri|Sat|Sun', - months: 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec', - dateFormat: '{y}/{m}/{d}', - timeFormat: '{y}/{m}/{d} {h}:{i}:{s}', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/content-script-resource.json b/src/i18n/message/common/content-script-resource.json new file mode 100644 index 000000000..fb4bd3117 --- /dev/null +++ b/src/i18n/message/common/content-script-resource.json @@ -0,0 +1,38 @@ +{ + "zh_CN": { + "consoleLog": "今天您打开了 {time} 次 {host},花费了 {focus} 来浏览它。", + "closeAlert": "你可以在【网费很贵】的选项中关闭以上提示!", + "timeWithHour": "{hour} 小时 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "您已被【{appName}】限制上网", + "more5Minutes": "再看 5 分钟!!我保证!" + }, + "zh_TW": { + "consoleLog": "今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。", + "closeAlert": "你可以在【網費很貴】的選項中關閉以上提示!", + "timeWithHour": "{hour} 小時 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "您已被【{appName}】限製上網", + "more5Minutes": "再看 5 分鐘!!我保証!" + }, + "en": { + "consoleLog": "You have open {host} for {time} time(s) and browsed it for {focus} today.", + "closeAlert": "You can turn off the above tips in the option of Timer!", + "timeWithHour": "{hour} hour(s) {minute} minute(s) {second} second(s)", + "timeWithMinute": "{minute} minute(s) {second} second(s)", + "timeWithSecond": "{second} second(s)", + "timeLimitMsg": "You have been restricted by [{appName}]", + "more5Minutes": "More 5 minutes, please!!" + }, + "ja": { + "consoleLog": "{host} を {time} 回開いて、今日 {focus} をブラウズしました。", + "closeAlert": "Timer のオプションで上記のヒントをオフにすることができます!", + "timeWithHour": "{hour} 時間 {minute} 分 {second} 秒", + "timeWithMinute": "{minute} 分 {second} 秒", + "timeWithSecond": "{second} 秒", + "timeLimitMsg": "【{appName}】によって制限されています", + "more5Minutes": "さらに5分間見てください! ! 約束します!" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/content-script.ts b/src/i18n/message/common/content-script.ts index 457567bc8..5da6e3809 100644 --- a/src/i18n/message/common/content-script.ts +++ b/src/i18n/message/common/content-script.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './content-script-resource.json' + export type ContentScriptMessage = { consoleLog: string closeAlert: string @@ -15,43 +17,6 @@ export type ContentScriptMessage = { more5Minutes: string } -const _default: Messages = { - zh_CN: { - consoleLog: '今天您打开了 {time} 次 {host},花费了 {focus} 来浏览它。', - closeAlert: '你可以在【网费很贵】的选项中关闭以上提示!', - timeWithHour: '{hour} 小时 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '您已被【{appName}】限制上网', - more5Minutes: '再看 5 分钟!!我保证!', - }, - zh_TW: { - consoleLog: '今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。', - closeAlert: '你可以在【網費很貴】的選項中關閉以上提示!', - timeWithHour: '{hour} 小時 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '您已被【{appName}】限製上網', - more5Minutes: '再看 5 分鐘!!我保証!', - }, - en: { - consoleLog: 'You have open {host} for {time} time(s) and browsed it for {focus} today.', - closeAlert: 'You can turn off the above tips in the option of Timer!', - timeWithHour: '{hour} hour(s) {minute} minute(s) {second} second(s)', - timeWithMinute: '{minute} minute(s) {second} second(s)', - timeWithSecond: '{second} second(s)', - timeLimitMsg: 'You have been restricted by [{appName}]', - more5Minutes: 'More 5 minutes, please!!', - }, - ja: { - consoleLog: '{host} を {time} 回開いて、今日 {focus} をブラウズしました。', - closeAlert: 'Timer のオプションで上記のヒントをオフにすることができます!', - timeWithHour: '{hour} 時間 {minute} 分 {second} 秒', - timeWithMinute: '{minute} 分 {second} 秒', - timeWithSecond: '{second} 秒', - timeLimitMsg: '【{appName}】によって制限されています', - more5Minutes: 'さらに5分間見てください! ! 約束します!', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/context-menus-resource.json b/src/i18n/message/common/context-menus-resource.json new file mode 100644 index 000000000..eec1a376d --- /dev/null +++ b/src/i18n/message/common/context-menus-resource.json @@ -0,0 +1,30 @@ +{ + "zh_CN": { + "add2Whitelist": "将{host}加入白名单", + "removeFromWhitelist": "将{host}从白名单移出", + "optionPage": "扩展选项", + "repoPage": "源码下载", + "feedbackPage": "吐槽一下" + }, + "zh_TW": { + "add2Whitelist": "將{host}加入白名單", + "removeFromWhitelist": "將{host}從白名單移出", + "optionPage": "擴充選項", + "repoPage": "源碼下載", + "feedbackPage": "吐槽一下" + }, + "en": { + "add2Whitelist": "Add {host} to the whitelist", + "removeFromWhitelist": "Remove {host} from the whitelist", + "optionPage": "Options", + "repoPage": "Source Code", + "feedbackPage": "Issues" + }, + "ja": { + "add2Whitelist": "ホワイトリストに {host} を追加", + "removeFromWhitelist": "ホワイトリストから {host} を削除します", + "optionPage": "拡張設定", + "repoPage": "ソースコード", + "feedbackPage": "フィードバックの欠如" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/context-menus.ts b/src/i18n/message/common/context-menus.ts index 09117dee4..43257605b 100644 --- a/src/i18n/message/common/context-menus.ts +++ b/src/i18n/message/common/context-menus.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './context-menus-resource.json' + /** * Used for menu */ @@ -16,35 +18,6 @@ export type ContextMenusMessage = { feedbackPage: string } -const _default: Messages = { - zh_CN: { - add2Whitelist: '将{host}加入白名单', - removeFromWhitelist: '将{host}从白名单移出', - optionPage: '扩展选项', - repoPage: '源码下载', - feedbackPage: '吐槽一下', - }, - zh_TW: { - add2Whitelist: '將{host}加入白名單', - removeFromWhitelist: '將{host}從白名單移出', - optionPage: '擴充選項', - repoPage: '源碼下載', - feedbackPage: '吐槽一下', - }, - en: { - add2Whitelist: 'Add {host} to the whitelist', - removeFromWhitelist: 'Remove {host} from the whitelist', - optionPage: 'Options', - repoPage: 'Source Code', - feedbackPage: 'Issues', - }, - ja: { - add2Whitelist: 'ホワイトリストに {host} を追加', - removeFromWhitelist: 'ホワイトリストから {host} を削除します', - optionPage: '拡張設定', - repoPage: 'ソースコード', - feedbackPage: 'フィードバックの欠如', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/initial-resource.json b/src/i18n/message/common/initial-resource.json new file mode 100644 index 000000000..6bd31ab64 --- /dev/null +++ b/src/i18n/message/common/initial-resource.json @@ -0,0 +1,34 @@ +{ + "zh_CN": { + "localFile": { + "json": "JSON 文件", + "pdf": "PDF 文件", + "pic": "图片文件", + "txt": "文本文件" + } + }, + "zh_TW": { + "localFile": { + "json": "JSON 文件", + "pdf": "PDF 文件", + "pic": "圖片文件", + "txt": "文本文件" + } + }, + "en": { + "localFile": { + "json": "JSON Files", + "pdf": "PDF Files", + "pic": "Images", + "txt": "Text Files" + } + }, + "ja": { + "localFile": { + "json": "JSON", + "pdf": "PDF", + "pic": "写真", + "txt": "TXT" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/initial.ts b/src/i18n/message/common/initial.ts index ab1a7f613..71efca187 100644 --- a/src/i18n/message/common/initial.ts +++ b/src/i18n/message/common/initial.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './initial-resource.json' + /** * Locales for initial data * @@ -19,39 +21,6 @@ export type InitialMessage = { } } -const _default: Messages = { - zh_CN: { - localFile: { - json: 'JSON 文件', - pdf: 'PDF 文件', - pic: '图片文件', - txt: '文本文件', - }, - }, - zh_TW: { - localFile: { - json: 'JSON 文件', - pdf: 'PDF 文件', - pic: '圖片文件', - txt: '文本文件', - }, - }, - en: { - localFile: { - json: 'JSON Files', - pdf: 'PDF Files', - pic: 'Images', - txt: 'Text Files', - }, - }, - ja: { - localFile: { - json: 'JSON', - pdf: 'PDF', - pic: '写真', - txt: 'TXT', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json new file mode 100644 index 000000000..e9dc8f08a --- /dev/null +++ b/src/i18n/message/common/item-resource.json @@ -0,0 +1,74 @@ +{ + "zh_CN": { + "date": "日期", + "host": "域名", + "focus": "浏览时长", + "time": "打开次数", + "operation": { + "label": "操作", + "delete": "删除", + "add2Whitelist": "白名单", + "removeFromWhitelist": "启用", + "analysis": "分析", + "deleteConfirmMsgAll": "{url} 的所有访问记录将被删除", + "deleteConfirmMsgRange": "{url} 在 {start} 到 {end} 的访问记录将被删除", + "deleteConfirmMsg": "{url} 在 {date} 的访问记录将被删除", + "exportWholeData": "导出数据", + "importWholeData": "导入数据" + } + }, + "zh_TW": { + "date": "日期", + "host": "域名", + "focus": "瀏覽時長", + "time": "訪問次數", + "operation": { + "label": "操作", + "delete": "刪除", + "add2Whitelist": "白名單", + "removeFromWhitelist": "啟用", + "analysis": "分析", + "deleteConfirmMsgAll": "{url} 的所有拜訪記錄將被刪除", + "deleteConfirmMsgRange": "{url} 在 {start} 到 {end} 的拜訪記錄將被刪除", + "deleteConfirmMsg": "{url} 在 {date} 的拜訪記錄將被刪除", + "exportWholeData": "導出數據", + "importWholeData": "導入數據" + } + }, + "en": { + "date": "Date", + "host": "Site URL", + "focus": "Browsing Time", + "time": "Site Visits", + "operation": { + "label": "Operations", + "delete": "Delete", + "add2Whitelist": "Whitelist", + "removeFromWhitelist": "Enable", + "analysis": "Analysis", + "deleteConfirmMsgAll": "All records of {url} will be deleted!", + "deleteConfirmMsgRange": "All records of {url} between {start} and {end} will be deleted!", + "deleteConfirmMsg": "The record of {url} on {date} will be deleted!", + "exportWholeData": "Export Data", + "importWholeData": "Import Data" + } + }, + "ja": { + "date": "日期", + "host": "URL", + "focus": "閲覧時間", + "time": "拜訪回数", + "operation": { + "label": "操作", + "delete": "削除", + "add2Whitelist": "ホワイトリスト", + "removeFromWhitelist": "有効にする", + "analysis": "分析する", + "deleteConfirmMsgAll": "{url} のすべての拜訪記録が削除されます", + "deleteConfirmMsgRange": "{url} {start} から {end} までの拜訪記録は削除されます", + "deleteConfirmMsg": "{date} の {url} の拜訪記録は削除されます", + "exportWholeData": "インポート", + "importWholeData": "書き出す" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/item.ts b/src/i18n/message/common/item.ts index c5355512c..7190f16a2 100644 --- a/src/i18n/message/common/item.ts +++ b/src/i18n/message/common/item.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './item-resource.json' + export type ItemMessage = { date: string host: string @@ -24,79 +26,6 @@ export type ItemMessage = { } } -const _default: Messages = { - zh_CN: { - date: '日期', - host: '域名', - focus: '浏览时长', - time: '打开次数', - operation: { - label: '操作', - delete: '删除', - add2Whitelist: '白名单', - removeFromWhitelist: '启用', - analysis: '分析', - deleteConfirmMsgAll: '{url} 的所有访问记录将被删除', - deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的访问记录将被删除', - deleteConfirmMsg: '{url} 在 {date} 的访问记录将被删除', - exportWholeData: '导出数据', - importWholeData: '导入数据', - }, - }, - zh_TW: { - date: '日期', - host: '域名', - focus: '瀏覽時長', - time: '訪問次數', - operation: { - label: '操作', - delete: '刪除', - add2Whitelist: '白名單', - removeFromWhitelist: '啟用', - analysis: '分析', - deleteConfirmMsgAll: '{url} 的所有拜訪記錄將被刪除', - deleteConfirmMsgRange: '{url} 在 {start} 到 {end} 的拜訪記錄將被刪除', - deleteConfirmMsg: '{url} 在 {date} 的拜訪記錄將被刪除', - exportWholeData: '導出數據', - importWholeData: '導入數據', - }, - }, - en: { - date: 'Date', - host: 'Site URL', - focus: 'Browsing Time', - time: 'Site Visits', - operation: { - label: 'Operations', - delete: 'Delete', - add2Whitelist: 'Whitelist', - removeFromWhitelist: 'Enable', - analysis: 'Analysis', - deleteConfirmMsgAll: 'All records of {url} will be deleted!', - deleteConfirmMsgRange: 'All records of {url} between {start} and {end} will be deleted!', - deleteConfirmMsg: 'The record of {url} on {date} will be deleted!', - exportWholeData: 'Export Data', - importWholeData: 'Import Data', - }, - }, - ja: { - date: '日期', - host: 'URL', - focus: '閲覧時間', - time: '拜訪回数', - operation: { - label: '操作', - delete: '削除', - add2Whitelist: 'ホワイトリスト', - removeFromWhitelist: '有効にする', - analysis: '分析する', - deleteConfirmMsgAll: '{url} のすべての拜訪記録が削除されます', - deleteConfirmMsgRange: '{url} {start} から {end} までの拜訪記録は削除されます', - deleteConfirmMsg: '{date} の {url} の拜訪記録は削除されます', - exportWholeData: 'インポート', - importWholeData: '書き出す', - }, - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/common/locale-resource.json b/src/i18n/message/common/locale-resource.json new file mode 100644 index 000000000..3c7d3cfa4 --- /dev/null +++ b/src/i18n/message/common/locale-resource.json @@ -0,0 +1,84 @@ +{ + "zh_CN": { + "name": "简体中文", + "comma": "," + }, + "zh_TW": { + "name": "正體中文", + "comma": "," + }, + "en": { + "name": "English", + "comma": ", " + }, + "ja": { + "name": "日本語", + "comma": "、" + }, + "pl": { + "name": "Polski" + }, + "pt": { + "name": "Português" + }, + "pt_BR": { + "name": "Portugues, Brasil" + }, + "ko": { + "name": "한국인" + }, + "de": { + "name": "Deutsch" + }, + "es": { + "name": "Español" + }, + "ru": { + "name": "Русский" + }, + "uk": { + "name": "українська" + }, + "fr": { + "name": "Français" + }, + "it": { + "name": "italiano" + }, + "sv": { + "name": "Sverige" + }, + "fi": { + "name": "Suomalainen" + }, + "da": { + "name": "dansk" + }, + "hr": { + "name": "Hrvatski" + }, + "id": { + "name": "bahasa Indonesia" + }, + "tr": { + "name": "Türkçe" + }, + "cs": { + "name": "čeština" + }, + "ro": { + "name": "Română" + }, + "nl": { + "name": "Nederlands" + }, + "vi": { + "name": "Tiếng Việt" + }, + "sk": { + "name": "slovenský" + }, + "mn": { + "name": "Монгол" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/locale.ts b/src/i18n/message/common/locale.ts index 135fa455d..ec1d4045a 100644 --- a/src/i18n/message/common/locale.ts +++ b/src/i18n/message/common/locale.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './locale-resource.json' + type MetaBase = { name: string } @@ -25,89 +27,6 @@ export type LocaleMessages = [translatingLocale in timer.TranslatingLocale]: MetaBase } -const _default: LocaleMessages = { - zh_CN: { - name: '简体中文', - comma: ',' - }, - zh_TW: { - name: '正體中文', - comma: ',', - }, - en: { - name: 'English', - comma: ', ' - }, - ja: { - name: '日本語', - comma: '、' - }, - pl: { - name: 'Polski' - }, - pt: { - name: 'Português' - }, - pt_BR: { - name: 'Portugues, Brasil' - }, - ko: { - name: '한국인' - }, - de: { - name: 'Deutsch' - }, - es: { - name: 'Español' - }, - ru: { - name: 'Русский' - }, - uk: { - name: "українська" - }, - fr: { - name: "Français" - }, - it: { - name: "italiano" - }, - sv: { - name: "Sverige" - }, - fi: { - name: "Suomalainen", - }, - da: { - name: "dansk", - }, - hr: { - name: "Hrvatski", - }, - id: { - name: "bahasa Indonesia", - }, - tr: { - name: "Türkçe", - }, - cs: { - name: "čeština", - }, - ro: { - name: "Română", - }, - nl: { - name: "Nederlands", - }, - vi: { - name: "Tiếng Việt", - }, - sk: { - name: "slovenský", - }, - mn: { - name: "Монгол", - }, -} +const _default: LocaleMessages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/merge-resource.json b/src/i18n/message/common/merge-resource.json new file mode 100644 index 000000000..268a1371c --- /dev/null +++ b/src/i18n/message/common/merge-resource.json @@ -0,0 +1,26 @@ +{ + "en": { + "tagResult": { + "blank": "Not Merge", + "level": "Keep Level {level}" + } + }, + "zh_CN": { + "tagResult": { + "blank": "不合并", + "level": "{level} 级域名" + } + }, + "zh_TW": { + "tagResult": { + "blank": "不合並", + "level": "{level} 級網域" + } + }, + "ja": { + "tagResult": { + "blank": "不合并", + "level": "{level} 次ドメイン" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/common/meta-resource.json b/src/i18n/message/common/meta-resource.json new file mode 100644 index 000000000..23a6018f5 --- /dev/null +++ b/src/i18n/message/common/meta-resource.json @@ -0,0 +1,24 @@ +{ + "zh_CN": { + "name": "网费很贵", + "marketName": "网费很贵 - 上网时间统计", + "description": "做最好用的上网时间统计工具。", + "slogan": "Insight & Improve" + }, + "zh_TW": { + "name": "網費很貴", + "marketName": "網費很貴 - 上網時間統計", + "description": "做最好用的上網時間統計工具。" + }, + "ja": { + "name": "Web時間統計", + "marketName": "ウェブタイムトラッカー", + "description": "最高のウェブタイムトラッカーになるために。" + }, + "en": { + "name": "Time Tracker", + "marketName": "Time Tracker", + "description": "To be the BEST webtime tracker.", + "slogan": "Insight & Improve" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index f271c248e..35f5b94e3 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './meta-resource.json' + export type MetaMessage = { name: string marketName: string @@ -14,29 +16,6 @@ export type MetaMessage = { const SLOGAN_EN = 'Insight & Improve' -const _default: Messages = { - zh_CN: { - name: '网费很贵', - marketName: '网费很贵 - 上网时间统计', - description: '做最好用的上网时间统计工具。', - slogan: SLOGAN_EN, - }, - zh_TW: { - name: '網費很貴', - marketName: '網費很貴 - 上網時間統計', - description: '做最好用的上網時間統計工具。', - }, - ja: { - name: 'Web時間統計', - marketName: 'ウェブタイムトラッカー', - description: '最高のウェブタイムトラッカーになるために。', - }, - en: { - name: 'Time Tracker', - marketName: 'Time Tracker', - description: 'To be the BEST webtime tracker.', - slogan: SLOGAN_EN, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration-resource.json b/src/i18n/message/common/popup-duration-resource.json new file mode 100644 index 000000000..3799c8dd7 --- /dev/null +++ b/src/i18n/message/common/popup-duration-resource.json @@ -0,0 +1,26 @@ +{ + "zh_CN": { + "today": "今日", + "thisWeek": "本周", + "thisMonth": "本月", + "last30Days": "近 30 天" + }, + "zh_TW": { + "today": "今日", + "thisWeek": "本週", + "thisMonth": "本月", + "last30Days": "近 30 天" + }, + "en": { + "today": "Today's", + "thisWeek": "This Week's", + "thisMonth": "This Month's", + "last30Days": "Last 30 days'" + }, + "ja": { + "today": "今日の", + "thisWeek": "今週の", + "thisMonth": "今月の", + "last30Days": "過去 30 日間" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration.ts b/src/i18n/message/common/popup-duration.ts index a79846ba7..b91145441 100644 --- a/src/i18n/message/common/popup-duration.ts +++ b/src/i18n/message/common/popup-duration.ts @@ -5,33 +5,10 @@ * https://opensource.org/licenses/MIT */ +import resource from './popup-duration-resource.json' + export type PopupDurationMessage = { [key in timer.option.PopupDuration]: string } -const _default: Messages = { - zh_CN: { - today: '今日', - thisWeek: '本周', - thisMonth: '本月', - last30Days: '近 30 天', - }, - zh_TW: { - today: '今日', - thisWeek: '本週', - thisMonth: '本月', - last30Days: '近 30 天', - }, - en: { - today: 'Today\'s', - thisWeek: 'This Week\'s', - thisMonth: 'This Month\'s', - last30Days: 'Last 30 days\'', - }, - ja: { - today: '今日の', - thisWeek: '今週の', - thisMonth: '今月の', - last30Days: '過去 30 日間', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/app-resource.json b/src/i18n/message/guide/app-resource.json new file mode 100644 index 000000000..15f50f913 --- /dev/null +++ b/src/i18n/message/guide/app-resource.json @@ -0,0 +1,30 @@ +{ + "en": { + "title": "Enter the management page", + "p1": "Based on icons, the extension provides a more convenient way to view data. But if you want to experience full functionality, you need to visit the management page of the extension, via one of the following two ways.", + "l1": "You can right-click the icon of the extension, and click [{button}] in the pop-up menu.", + "l2": "You can also find the [{button}] link at the bottom of the icon popup page, just click it.", + "p2": "The popup page and management page are the main interaction methods of this extension. After you know how to open them, you can use it completely." + }, + "zh_CN": { + "title": "进入后台管理页面", + "p1": "基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的后台管理页。进入后台页有以下两种方式。", + "l1": "您可以右击扩展的图标,在弹出的菜单中点击【{button}】。", + "l2": "您也可以在图标弹出页的页脚找到【{button}】链接,同样地,点击它即可。", + "p2": "弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。" + }, + "zh_TW": { + "title": "進入管理頁面", + "p1": "基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的後台管理頁。進入後台頁有以下兩種方式。", + "l1": "您可以右擊擴展的圖標,在彈出的菜單中點擊【{button}】。", + "l2": "您也可以在圖標彈出頁的頁腳找到【{button}】鏈接,同樣地,點擊它即可。", + "p2": "彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。" + }, + "ja": { + "title": "管理ページに入る", + "p1": "アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、その完全な機能を体験したい場合は、拡張バックグラウンド管理ページにアクセスする必要があります. バックグラウンド ページに入る方法は 2 つあります。", + "l1": "拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。", + "l2": "アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。", + "p2": "ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。" + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/app.ts b/src/i18n/message/guide/app.ts index a977f7054..9bfd49ddf 100644 --- a/src/i18n/message/guide/app.ts +++ b/src/i18n/message/guide/app.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './app-resource.json' + export type AppMessage = { title: string p1: string @@ -13,37 +15,6 @@ export type AppMessage = { p2: string } -const _default: Messages = { - en: { - title: 'Enter the management page', - p1: 'Based on icons, the extension provides a more convenient way to view data. ' - + 'But if you want to experience full functionality, ' - + 'you need to visit the management page of the extension, via one of the following two ways.', - l1: 'You can right-click the icon of the extension, and click [{button}] in the pop-up menu.', - l2: 'You can also find the [{button}] link at the bottom of the icon popup page, just click it.', - p2: 'The popup page and management page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', - }, - zh_CN: { - title: '进入后台管理页面', - p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的后台管理页。进入后台页有以下两种方式。', - l1: '您可以右击扩展的图标,在弹出的菜单中点击【{button}】。', - l2: '您也可以在图标弹出页的页脚找到【{button}】链接,同样地,点击它即可。', - p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。' - }, - zh_TW: { - title: '進入管理頁面', - p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的後台管理頁。進入後台頁有以下兩種方式。', - l1: '您可以右擊擴展的圖標,在彈出的菜單中點擊【{button}】。', - l2: '您也可以在圖標彈出頁的頁腳找到【{button}】鏈接,同樣地,點擊它即可。', - p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', - }, - ja: { - title: '管理ページに入る', - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、その完全な機能を体験したい場合は、拡張バックグラウンド管理ページにアクセスする必要があります. バックグラウンド ページに入る方法は 2 つあります。', - l1: '拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。', - l2: 'アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。', - p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/backup-resource.json b/src/i18n/message/guide/backup-resource.json new file mode 100644 index 000000000..1a38823c9 --- /dev/null +++ b/src/i18n/message/guide/backup-resource.json @@ -0,0 +1,78 @@ +{ + "en": { + "title": "Backup data with GitHub Gist", + "p1": "This extension supports users to backup data using GitHub Gist{link} through simple settings.", + "upload": { + "title": "Four simple steps to complete the setup", + "prepareToken": "First, generate a token with gist permissions on GitHub{link}.", + "enter": "Enter the option page{link}.", + "form": "Then select GitHub Gist as the synchronization method, and fill in your token in the input box that appears below.", + "backup": "Click the Backup button to upload local data to your GitHub Gist." + }, + "query": { + "title": "How to query data backed up by other browsers?", + "p1": "If you correctly set the token in the above steps, you can query remote data in just three simple steps.", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "enable": "If the token is set correctly, an icon, like {icon}, will appear in the upper right corner of the page, click it to enable the remote query.", + "wait": "Wait for the data query to complete, and move the mouse over the value to view the data of each client.", + "tip": "Because remote data is stored in monthly shards, the query time period should not be too long." + } + }, + "zh_CN": { + "title": "使用 GitHub Gist 备份数据", + "p1": "这个扩展支持用户通过简单的设置,使用 GitHub Gist{link} 备份数据。", + "upload": { + "title": "简单四步完成设置", + "prepareToken": "首先,您需要在 GitHub 生成一个包含 gist 权限的 token{link}。", + "enter": "进入扩展的选项页面{link}。", + "form": "然后将同步方式选为 GitHub Gist,将你的 token 填入下方出现的输入框中。", + "backup": "最后,点击备份按钮即可将本地数据导入到你的 gist 里。" + }, + "query": { + "title": "如何查询其他浏览器备份的数据?", + "p1": "如果您在上述步骤中正确设置了 token,只需简单三步即可查询远端数据。", + "enter": "首先,进入管理页{link},点击菜单项【{menuItem}】。", + "enable": "如果 token 设置正确,页面右上角会出现一个{icon}图标,点击它即可开启远端查询。", + "wait": "等待数据查询完毕,将鼠标移动到数值上,即可查看每个客户端的数据。", + "tip": "因为远端数据时按月份分片存放,所以查询时间段不宜过长。" + } + }, + "zh_TW": { + "title": "使用 GitHub Gist 備份數據", + "p1": "這個擴展支持用戶通過簡單的設置,使用 GitHub Gist{link} 備份數據。", + "upload": { + "title": "簡單四步完成設置", + "prepareToken": "首先,您需要在 GitHub 生成一個包含 gist 權限的 token{link}。", + "enter": "進入擴展的選項頁面{link}。", + "form": "然後將同步方式選為 GitHub Gist,將你的 token 填入下方出現的輸入框中。", + "backup": "最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。" + }, + "query": { + "title": "如何查詢其他瀏覽器備份的數據?", + "p1": "如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。", + "enter": "首先,進入管理頁{link},點擊菜單項【{menuItem}】。", + "enable": "如果 token 設置正確,頁面右上角會出現一個{icon}圖標,點擊它即可開啟遠端查詢。", + "wait": "等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。", + "tip": "因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。" + } + }, + "ja": { + "title": "GitHub Gist でデータをバックアップする", + "p1": "この拡張機能は、簡単な設定で GitHub Gist{link} を使用してデータをバックアップするユーザーをサポートします。", + "upload": { + "title": "セットアップを完了するための 4 つの簡単なステップ", + "prepareToken": "まず、GitHub{link} で Gist 権限を持つトークンを生成します。", + "enter": "オプションページ{link}に入ります。", + "form": "次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。", + "backup": "[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。" + }, + "query": { + "title": "他のブラウザでバックアップされたデータを照会する方法は?", + "p1": "上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。", + "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "enable": "トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。", + "wait": "データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。", + "tip": "リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/backup.ts b/src/i18n/message/guide/backup.ts index f21e5b49f..46f6bb1a1 100644 --- a/src/i18n/message/guide/backup.ts +++ b/src/i18n/message/guide/backup.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './backup-resource.json' + export type BackupMessage = { title: String p1: string @@ -25,85 +27,6 @@ export type BackupMessage = { } } -const _default: Messages = { - en: { - title: 'Backup data with GitHub Gist', - p1: 'This extension supports users to backup data using GitHub Gist{link} through simple settings.', - upload: { - title: 'Four simple steps to complete the setup', - prepareToken: 'First, generate a token with gist permissions on GitHub{link}.', - enter: 'Enter the option page{link}.', - form: 'Then select GitHub Gist as the synchronization method, ' - + 'and fill in your token in the input box that appears below.', - backup: 'Click the Backup button to upload local data to your GitHub Gist.', - }, - query: { - title: 'How to query data backed up by other browsers?', - p1: 'If you correctly set the token in the above steps, you can query remote data in just three simple steps.', - enter: 'First, enter the management page{link}, click the menu item {menuItem}.', - enable: 'If the token is set correctly, an icon, like {icon}, will appear in the upper right corner of the page, ' - + 'click it to enable the remote query.', - wait: 'Wait for the data query to complete, and move the mouse over the value to view the data of each client.', - tip: 'Because remote data is stored in monthly shards, the query time period should not be too long.', - } - }, - zh_CN: { - title: '使用 GitHub Gist 备份数据', - p1: '这个扩展支持用户通过简单的设置,使用 GitHub Gist{link} 备份数据。', - upload: { - title: '简单四步完成设置', - prepareToken: '首先,您需要在 GitHub 生成一个包含 gist 权限的 token{link}。', - enter: '进入扩展的选项页面{link}。', - form: '然后将同步方式选为 GitHub Gist,将你的 token 填入下方出现的输入框中。', - backup: '最后,点击备份按钮即可将本地数据导入到你的 gist 里。' - }, - query: { - title: '如何查询其他浏览器备份的数据?', - p1: '如果您在上述步骤中正确设置了 token,只需简单三步即可查询远端数据。', - enter: '首先,进入管理页{link},点击菜单项【{menuItem}】。', - enable: '如果 token 设置正确,页面右上角会出现一个{icon}图标,点击它即可开启远端查询。', - wait: '等待数据查询完毕,将鼠标移动到数值上,即可查看每个客户端的数据。', - tip: '因为远端数据时按月份分片存放,所以查询时间段不宜过长。', - } - }, - zh_TW: { - title: '使用 GitHub Gist 備份數據', - p1: '這個擴展支持用戶通過簡單的設置,使用 GitHub Gist{link} 備份數據。', - upload: { - title: '簡單四步完成設置', - prepareToken: '首先,您需要在 GitHub 生成一個包含 gist 權限的 token{link}。', - enter: '進入擴展的選項頁面{link}。', - form: '然後將同步方式選為 GitHub Gist,將你的 token 填入下方出現的輸入框中。', - backup: '最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。' - }, - query: { - title: '如何查詢其他瀏覽器備份的數據?', - p1: '如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。', - enter: '首先,進入管理頁{link},點擊菜單項【{menuItem}】。', - enable: '如果 token 設置正確,頁面右上角會出現一個{icon}圖標,點擊它即可開啟遠端查詢。', - wait: '等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。', - tip: '因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。', - } - }, - ja: { - title: 'GitHub Gist でデータをバックアップする', - p1: 'この拡張機能は、簡単な設定で GitHub Gist{link} を使用してデータをバックアップするユーザーをサポートします。', - upload: { - title: 'セットアップを完了するための 4 つの簡単なステップ', - prepareToken: 'まず、GitHub{link} で Gist 権限を持つトークンを生成します。', - enter: 'オプションページ{link}に入ります。', - form: '次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。', - backup: '[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。', - }, - query: { - title: '他のブラウザでバックアップされたデータを照会する方法は?', - p1: '上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。', - enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', - enable: 'トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。', - wait: 'データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。', - tip: 'リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。', - } - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/guide/home-resource.json b/src/i18n/message/guide/home-resource.json new file mode 100644 index 000000000..55563ccb3 --- /dev/null +++ b/src/i18n/message/guide/home-resource.json @@ -0,0 +1,22 @@ +{ + "en": { + "desc": "Timer can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.", + "button": "Start Now!", + "download": "Install for {browser}" + }, + "zh_CN": { + "desc": "网费很贵是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。", + "button": "如何使用", + "download": "在 {browser} 上安装" + }, + "zh_TW": { + "desc": "網費很貴是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。", + "button": "如何使用", + "download": "在 {browser} 上安裝" + }, + "ja": { + "desc": "この拡張機能は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ち、ウェブ習慣を洞察して改善することができます.", + "button": "すぐに始めましょう", + "download": "{browser} にインストール" + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/home.ts b/src/i18n/message/guide/home.ts index 58197aff5..5b3d881e8 100644 --- a/src/i18n/message/guide/home.ts +++ b/src/i18n/message/guide/home.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './home-resource.json' + type _Key = | 'desc' | 'button' @@ -14,27 +16,6 @@ export type HomeMessage = { [key in _Key]: string } -const _default: Messages = { - en: { - desc: 'Timer can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.', - button: 'Start Now!', - download: 'Install for {browser}', - }, - zh_CN: { - desc: '网费很贵是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。', - button: '如何使用', - download: '在 {browser} 上安装', - }, - zh_TW: { - desc: '網費很貴是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。', - button: '如何使用', - download: '在 {browser} 上安裝', - }, - ja: { - desc: 'この拡張機能は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ち、ウェブ習慣を洞察して改善することができます.', - button: 'すぐに始めましょう', - download: '{browser} にインストール', - } -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/guide/layout-resource.json b/src/i18n/message/guide/layout-resource.json new file mode 100644 index 000000000..0d6c11e9a --- /dev/null +++ b/src/i18n/message/guide/layout-resource.json @@ -0,0 +1,38 @@ +{ + "zh_CN": { + "header": { + "sourceCode": "查看源代码", + "email": "联系作者" + }, + "menu": { + "usage": "高级用法" + } + }, + "zh_TW": { + "header": { + "sourceCode": "查看源代碼", + "email": "聯繫作者" + }, + "menu": { + "usage": "高級用法" + } + }, + "en": { + "header": { + "sourceCode": "View source code", + "email": "Contact author" + }, + "menu": { + "usage": "Advanced usages" + } + }, + "ja": { + "header": { + "sourceCode": "ソースコードを見る", + "email": "著者に連絡する" + }, + "menu": { + "usage": "高度な使い方" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 26cd489f1..9a1302636 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './layout-resource.json' + export type LayoutMessage = { header: { sourceCode: string @@ -15,43 +17,6 @@ export type LayoutMessage = { } } -const _default: Messages = { - zh_CN: { - header: { - sourceCode: '查看源代码', - email: '联系作者', - }, - menu: { - usage: '高级用法' - }, - }, - zh_TW: { - header: { - sourceCode: '查看源代碼', - email: '聯繫作者', - }, - menu: { - usage: '高級用法', - }, - }, - en: { - header: { - sourceCode: 'View source code', - email: 'Contact author' - }, - menu: { - usage: 'Advanced usages', - }, - }, - ja: { - header: { - sourceCode: 'ソースコードを見る', - email: '著者に連絡する', - }, - menu: { - usage: '高度な使い方', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/limit-resource.json b/src/i18n/message/guide/limit-resource.json new file mode 100644 index 000000000..7ca63f445 --- /dev/null +++ b/src/i18n/message/guide/limit-resource.json @@ -0,0 +1,46 @@ +{ + "en": { + "title": "Limit browsing time of everyday", + "p1": "If you want to limit the time of browsing certain URLs each day, you can do so by creating a daily time limit rule.", + "step": { + "title": "Four steps to create a limit rule", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "click": "Click the New button in the upper right corner.", + "form": "Paste the URL that needs to be restricted, and the duration of the restriction. Some URL fragments can be replaced with wildcards as needed, or deleted directly. Then click Save.", + "check": "Finally, check whether the target URL hits the newly added rule through the Test button in the upper right corner." + } + }, + "zh_CN": { + "title": "限制每天的浏览时间", + "p1": "如果你想限制每天浏览某些 URL 的时长,可以通过创建每日时限规则来完成。", + "step": { + "title": "简单四步创建一个限制规则", + "enter": "首先进入管理页{link},点击菜单项【{menuItem}】。", + "click": "然后单击右上角的新建按钮。", + "form": "粘贴需要限制的 URL,以及限制时长。可以根据需要将部分 URL 片段使用通配符代替,或者直接删除。然后点击保存。", + "check": "最后通过右上角的测试功能检查目标 URL 是否命中了刚添加的规则。" + } + }, + "zh_TW": { + "title": "限制每天的瀏覽時間", + "p1": "如果你想限制每天瀏覽某些 URL 的時長,可以通過創建每日時限規則來完成。", + "step": { + "title": "簡單四步創建一個限制規則", + "enter": "首先進入管理頁{link},點擊菜單項【{menuItem}】。", + "click": "然後單擊右上角的新建按鈕。", + "form": "粘貼需要限制的 URL,以及限制時長。可以根據需要將部分 URL 片段使用通配符代替,或者直接刪除。然後點擊保存。", + "check": "最後通過右上角的測試功能檢查目標 URL 是否命中了剛添加的規則。" + } + }, + "ja": { + "title": "毎日の閲覧時間を制限する", + "p1": "特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。", + "step": { + "title": "制限ルールを作成するための 4 つのステップ", + "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "click": "右上隅にある [新規] ボタンをクリックします。", + "form": "制限する必要がある URL と制限の期間を貼り付けます。一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。", + "check": "最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/limit.ts b/src/i18n/message/guide/limit.ts index a9bf84895..3325ee1b9 100644 --- a/src/i18n/message/guide/limit.ts +++ b/src/i18n/message/guide/limit.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './limit-resource.json' + export type LimitMessage = { title: string p1: string @@ -17,53 +19,6 @@ export type LimitMessage = { } } -const _default: Messages = { - en: { - title: 'Limit browsing time of everyday', - p1: 'If you want to limit the time of browsing certain URLs each day, you can do so by creating a daily time limit rule.', - step: { - title: 'Four steps to create a limit rule', - enter: 'First, enter the management page{link}, click the menu item {menuItem}.', - click: 'Click the New button in the upper right corner.', - form: 'Paste the URL that needs to be restricted, and the duration of the restriction. ' - + 'Some URL fragments can be replaced with wildcards as needed, or deleted directly. Then click Save.', - check: 'Finally, check whether the target URL hits the newly added rule through the Test button in the upper right corner.', - } - }, - zh_CN: { - title: '限制每天的浏览时间', - p1: '如果你想限制每天浏览某些 URL 的时长,可以通过创建每日时限规则来完成。', - step: { - title: '简单四步创建一个限制规则', - enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', - click: '然后单击右上角的新建按钮。', - form: '粘贴需要限制的 URL,以及限制时长。可以根据需要将部分 URL 片段使用通配符代替,或者直接删除。然后点击保存。', - check: '最后通过右上角的测试功能检查目标 URL 是否命中了刚添加的规则。', - } - }, - zh_TW: { - title: '限制每天的瀏覽時間', - p1: '如果你想限制每天瀏覽某些 URL 的時長,可以通過創建每日時限規則來完成。', - step: { - title: '簡單四步創建一個限制規則', - enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', - click: '然後單擊右上角的新建按鈕。', - form: '粘貼需要限制的 URL,以及限制時長。可以根據需要將部分 URL 片段使用通配符代替,或者直接刪除。然後點擊保存。', - check: '最後通過右上角的測試功能檢查目標 URL 是否命中了剛添加的規則。', - } - }, - ja: { - title: '毎日の閲覧時間を制限する', - p1: '特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。', - step: { - title: '制限ルールを作成するための 4 つのステップ', - enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', - click: '右上隅にある [新規] ボタンをクリックします。', - form: '制限する必要がある URL と制限の期間を貼り付けます。' - + '一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。', - check: '最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。', - } - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/guide/merge-resource.json b/src/i18n/message/guide/merge-resource.json new file mode 100644 index 000000000..92b9f995c --- /dev/null +++ b/src/i18n/message/guide/merge-resource.json @@ -0,0 +1,130 @@ +{ + "en": { + "title": "Summarize data of related sites", + "p1": "This extension is counted by domain name, for example, {demo1} and {demo2} will be counted as 2 records. If you want to see aggregated data for both sites, you'll need to use the merge feature.", + "p2": "On most data display pages, merged queries are supported. And users can customize the merge rules on the background page{link}.", + "lookTitle": "What's rules look like?", + "p3": "The rule consists of two parts, the source part and the target part. The source part declares which sites hit the rule, and the target part defines how those sites are merged. For example, {demo} is a common rule, the left is the source site, and the right is the target site.", + "sourceCol": "Source part", + "targetCol": "Target part", + "remarkCol": "Remark", + "source": { + "title": "How to define the source part?", + "p1": "The source part can be a specific domain name or an Ant expression. Below are some examples.", + "exampleCol": "Examples of matched site", + "only": "Only {source} can hit this rule" + }, + "target": { + "title": "How to define the target part?", + "p1": "The target part can be a specific domain name, a positive integer, or be left blank. They will be introduced one by one in conjunction with the source part in the table below.", + "lookCol": "Look", + "remark": { + "blank": "{source} won't be merged cause of blank target part", + "spec": "Sites hitting {source} will be merged into the specific {target}", + "integer": "Sites hitting {source} will be merged into the last {target} level domain names", + "specFirst": "When multiple rules are hit, the source part takes precedence for a specific domain name", + "miss": "Merge to the before level of Public Suffix List{link} when no rules are hit" + }, + "p2": "The following table is some merging examples after the above rules are set at the same time.", + "originalCol": "Original site", + "mergedCol": "Merged site", + "hitCol": "Hitted rule" + } + }, + "zh_CN": { + "title": "合并统计相关站点", + "p1": "这个扩展是按照域名进行统计的,比方说 {demo1} 和 {demo2} 会被统计到 2 个条目。如果您想要查看这两个网站的数据汇总的话,就需要使用合并功能。", + "p2": "在大多数的数据展示界面,都支持站点合并查询。并且用户可以在后台页自定义合并规则{link}。", + "lookTitle": "规则长什么样?", + "p3": "规则由两部分组成,源和目标。源部分声明哪些站点会命中该规则,而目标部分定义如何合并这些站点。比如 {demo} 就是一个常见的规则,左边的是源,右边的是目标。", + "source": { + "title": "如何定义源部分?", + "p1": "源部分可以是具体的域名,也可以是 Ant 表达式。下面是一些例子。", + "exampleCol": "匹配网站的示例", + "only": "只有 {source} 能够命中该规则" + }, + "sourceCol": "源部分", + "targetCol": "目标部分", + "remarkCol": "备注", + "target": { + "title": "如何定义目标部分?", + "p1": "目标部分可以是具体的域名,正整数,或者留空。将在下表中结合源部分一一介绍。", + "lookCol": "规则外观", + "remark": { + "blank": "{source} 不会被合并,因为目标部分为空", + "spec": "满足 {source} 的网站会被合并到指定条目 {target}", + "integer": "满足 {source} 的网站,在合并时会保留后 {target} 级域名", + "specFirst": "命中多个规则时,源部分是具体域名的优先", + "miss": "没有命中任何规则时,合并至 Public Suffix List{link} 的前一级" + }, + "p2": "下表是上述规则同时设置后的一些合并示例。", + "originalCol": "原始站点", + "mergedCol": "合并后站点", + "hitCol": "命中的规则" + } + }, + "zh_TW": { + "title": "合併統計相關站點", + "p1": "這個擴展是按照域名進行統計的,比方說 {demo1} 和 {demo2} 會被統計到 2 個條目。如果您想要查看這兩個網站的數據匯總的話,就需要使用合併功能。", + "p2": "在大多數的數據展示界面,都支持站點合併查詢。並且用戶可以在後台頁自定義合併規則{link}。", + "lookTitle": "規則長什麼樣?", + "p3": "規則由兩部分組成,源和目標。源部分聲明哪些站點會命中該規則,而目標部分定義如何合併這些站點。比如 {demo} 就是一個常見的規則,左邊的是源,右邊的是目標。", + "source": { + "title": "如何定義源部分?", + "p1": "源部分可以是具體的域名,也可以是 Ant 表達式。下面是一些例子。", + "exampleCol": "匹配網站的示例", + "only": "只有 {source} 能够命中该规则" + }, + "sourceCol": "源部分", + "targetCol": "目標部分", + "remarkCol": "備註", + "target": { + "title": "如何定義目標部分?", + "p1": "目標部分可以是具體的域名,正整數,或者留空。將在下表中結合源部分一一介紹。", + "lookCol": "規則外觀", + "remark": { + "blank": "{source} 不会被合并,因为目标部分为空", + "spec": "滿足 {source} 的網站會被合併到指定條目 {target}", + "integer": "滿足 {source} 的網站,在合併時會保留後 {target} 級域名", + "specFirst": "命中多個規則時,源部分是具體域名的優先", + "miss": "沒有命中任何規則時,合併至 Public Suffix List{link} 的前一級" + }, + "p2": "下表是上述規則同時設置後的一些合併示例。", + "originalCol": "原始站點", + "mergedCol": "合併後站點", + "hitCol": "命中的規則" + } + }, + "ja": { + "title": "関連サイトのデータをまとめます", + "p1": "この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。", + "p2": "ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ{link}でマージ ルールをカスタマイズできます。", + "lookTitle": "ルールはどのように見えますか?", + "p3": "ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。", + "sourceCol": "ソース部分", + "targetCol": "対象部位", + "remarkCol": "述べる", + "source": { + "title": "ソース パーツを定義する方法", + "p1": "ソース部分は、特定のドメイン名または Ant 式にすることができます。 以下にいくつかの例を示します。", + "exampleCol": "マッチしたサイトの例", + "only": "このルールに該当するのは {source} だけです" + }, + "target": { + "title": "ターゲット パーツを定義する方法", + "p1": "ターゲット部分は、特定のドメイン名、正の整数、または空白のままにすることができます。下表のソース部分と合わせて順次紹介していきます。", + "lookCol": "外観", + "remark": { + "blank": "{source} はマージされません 空白のターゲット パーツが原因です", + "spec": "{source} にヒットしたサイトは、特定の {target} にマージされます", + "integer": "{source} にヒットしたサイトは、最後の {target} レベルのドメイン名にマージされます", + "specFirst": "複数のルールがヒットした場合、特定のドメイン名についてソース部分が優先されます", + "miss": "ルールにヒットしない場合は、Public Suffix List{link} の前のレベルにマージします" + }, + "p2": "次の表は、上記のルールを同時に設定した後のいくつかのマージ例です。", + "originalCol": "元のサイト", + "mergedCol": "統合サイト", + "hitCol": "ヒットルール" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/merge.ts b/src/i18n/message/guide/merge.ts index a6168ee12..f5dbc8752 100644 --- a/src/i18n/message/guide/merge.ts +++ b/src/i18n/message/guide/merge.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './merge-resource.json' + export type MergeMessage = { title: string p1: string @@ -38,147 +40,6 @@ export type MergeMessage = { } } -const _default: Messages = { - en: { - title: 'Summarize data of related sites', - p1: 'This extension is counted by domain name, for example, {demo1} and {demo2} will be counted as 2 records. ' - + 'If you want to see aggregated data for both sites, you\'ll need to use the merge feature.', - p2: 'On most data display pages, merged queries are supported. And users can customize the merge rules on the background page{link}.', - lookTitle: 'What\'s rules look like?', - p3: 'The rule consists of two parts, the source part and the target part. ' - + 'The source part declares which sites hit the rule, and the target part defines how those sites are merged. ' - + 'For example, {demo} is a common rule, the left is the source site, and the right is the target site.', - sourceCol: 'Source part', - targetCol: 'Target part', - remarkCol: 'Remark', - source: { - title: 'How to define the source part?', - p1: 'The source part can be a specific domain name or an Ant expression. Below are some examples.', - exampleCol: 'Examples of matched site', - only: "Only {source} can hit this rule" - }, - target: { - title: 'How to define the target part?', - p1: 'The target part can be a specific domain name, a positive integer, or be left blank. ' - + 'They will be introduced one by one in conjunction with the source part in the table below.', - - lookCol: 'Look', - remark: { - blank: '{source} won\'t be merged cause of blank target part', - spec: 'Sites hitting {source} will be merged into the specific {target}', - integer: 'Sites hitting {source} will be merged into the last {target} level domain names', - specFirst: 'When multiple rules are hit, the source part takes precedence for a specific domain name', - miss: 'Merge to the before level of Public Suffix List{link} when no rules are hit', - }, - p2: 'The following table is some merging examples after the above rules are set at the same time.', - originalCol: 'Original site', - mergedCol: 'Merged site', - hitCol: 'Hitted rule', - } - }, - zh_CN: { - title: '合并统计相关站点', - p1: '这个扩展是按照域名进行统计的,比方说 {demo1} 和 {demo2} 会被统计到 2 个条目。' - + '如果您想要查看这两个网站的数据汇总的话,就需要使用合并功能。', - p2: '在大多数的数据展示界面,都支持站点合并查询。并且用户可以在后台页自定义合并规则{link}。', - lookTitle: '规则长什么样?', - p3: '规则由两部分组成,源和目标。源部分声明哪些站点会命中该规则,而目标部分定义如何合并这些站点。' - + '比如 {demo} 就是一个常见的规则,左边的是源,右边的是目标。', - source: { - title: '如何定义源部分?', - p1: '源部分可以是具体的域名,也可以是 Ant 表达式。下面是一些例子。', - exampleCol: '匹配网站的示例', - only: '只有 {source} 能够命中该规则', - }, - sourceCol: '源部分', - targetCol: '目标部分', - remarkCol: '备注', - target: { - title: '如何定义目标部分?', - p1: '目标部分可以是具体的域名,正整数,或者留空。将在下表中结合源部分一一介绍。', - lookCol: '规则外观', - remark: { - blank: '{source} 不会被合并,因为目标部分为空', - spec: '满足 {source} 的网站会被合并到指定条目 {target}', - integer: '满足 {source} 的网站,在合并时会保留后 {target} 级域名', - specFirst: '命中多个规则时,源部分是具体域名的优先', - miss: '没有命中任何规则时,合并至 Public Suffix List{link} 的前一级', - }, - p2: '下表是上述规则同时设置后的一些合并示例。', - originalCol: '原始站点', - mergedCol: '合并后站点', - hitCol: '命中的规则', - }, - }, - zh_TW: { - title: '合併統計相關站點', - p1: '這個擴展是按照域名進行統計的,比方說 {demo1} 和 {demo2} 會被統計到 2 個條目。如果您想要查看這兩個網站的數據匯總的話,就需要使用合併功能。', - p2: '在大多數的數據展示界面,都支持站點合併查詢。並且用戶可以在後台頁自定義合併規則{link}。', - lookTitle: '規則長什麼樣?', - p3: '規則由兩部分組成,源和目標。源部分聲明哪些站點會命中該規則,而目標部分定義如何合併這些站點。比如 {demo} 就是一個常見的規則,左邊的是源,右邊的是目標。', - source: { - title: '如何定義源部分?', - p1: '源部分可以是具體的域名,也可以是 Ant 表達式。下面是一些例子。', - exampleCol: '匹配網站的示例', - only: '只有 {source} 能够命中该规则', - }, - sourceCol: '源部分', - targetCol: '目標部分', - remarkCol: '備註', - target: { - title: '如何定義目標部分?', - p1: '目標部分可以是具體的域名,正整數,或者留空。將在下表中結合源部分一一介紹。', - lookCol: '規則外觀', - remark: { - blank: '{source} 不会被合并,因为目标部分为空', - spec: '滿足 {source} 的網站會被合併到指定條目 {target}', - integer: '滿足 {source} 的網站,在合併時會保留後 {target} 級域名', - specFirst: '命中多個規則時,源部分是具體域名的優先', - miss: '沒有命中任何規則時,合併至 Public Suffix List{link} 的前一級', - }, - p2: '下表是上述規則同時設置後的一些合併示例。', - originalCol: '原始站點', - mergedCol: '合併後站點', - hitCol: '命中的規則', - }, - }, - ja: { - title: '関連サイトのデータをまとめます', - p1: 'この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。' - + '両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。', - p2: 'ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ{link}でマージ ルールをカスタマイズできます。', - lookTitle: 'ルールはどのように見えますか?', - p3: 'ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。' - + 'ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。' - + 'たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。', - sourceCol: 'ソース部分', - targetCol: '対象部位', - remarkCol: '述べる', - source: { - title: 'ソース パーツを定義する方法', - p1: 'ソース部分は、特定のドメイン名または Ant 式にすることができます。 以下にいくつかの例を示します。', - exampleCol: 'マッチしたサイトの例', - only: "このルールに該当するのは {source} だけです" - }, - target: { - title: 'ターゲット パーツを定義する方法', - p1: 'ターゲット部分は、特定のドメイン名、正の整数、または空白のままにすることができます。' - + '下表のソース部分と合わせて順次紹介していきます。', - - lookCol: '外観', - remark: { - blank: '{source} はマージされません 空白のターゲット パーツが原因です', - spec: '{source} にヒットしたサイトは、特定の {target} にマージされます', - integer: '{source} にヒットしたサイトは、最後の {target} レベルのドメイン名にマージされます', - specFirst: '複数のルールがヒットした場合、特定のドメイン名についてソース部分が優先されます', - miss: 'ルールにヒットしない場合は、Public Suffix List{link} の前のレベルにマージします', - }, - p2: '次の表は、上記のルールを同時に設定した後のいくつかのマージ例です。', - originalCol: '元のサイト', - mergedCol: '統合サイト', - hitCol: 'ヒットルール', - } - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/guide/privacy-resource.json b/src/i18n/message/guide/privacy-resource.json new file mode 100644 index 000000000..2347b6494 --- /dev/null +++ b/src/i18n/message/guide/privacy-resource.json @@ -0,0 +1,138 @@ +{ + "zh_CN": { + "title": "隐私声明", + "alert": "为了向您提供完整的服务,该扩展在使用过程中会必要地收集您的一些个人数据,详情见以下隐私声明。", + "scope": { + "title": "哪些数据会被收集?", + "cols": { + "name": "内容", + "usage": "用途", + "required": "是否必需" + }, + "rows": { + "website": { + "name": "网站访问记录", + "usage": "用于统计浏览时长和访问次数" + }, + "tab": { + "name": "浏览器标签信息", + "usage": "用于自动获取网站的名称和图标,展示数据时提升用户体验", + "optionalReason": "只有在选项里开启自动获取功能后才会收集" + }, + "clipboard": { + "name": "剪切板内容", + "usage": "在设置每日时限规则时,为了操作方便,会读取剪切板内 URL", + "optionalReason": "需要用户手动同意" + } + } + }, + "storage": { + "title": "如何处理这些数据?", + "p1": "我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。", + "p2": "不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。", + "p3": "我们只帮助您收集数据,但处置权一定在您。" + } + }, + "zh_TW": { + "title": "隱私聲明", + "alert": "為了向您提供完整的服務,該擴展在使用過程中會必要地收集您的一些個人數據,詳情見以下隱私聲明。", + "scope": { + "title": "哪些數據會被收集?", + "cols": { + "name": "內容", + "usage": "用途", + "required": "是否必需" + }, + "rows": { + "website": { + "name": "網站訪問記錄", + "usage": "用於統計瀏覽時長和訪問次數" + }, + "tab": { + "name": "瀏覽器標籤信息", + "usage": "用於自動獲取網站的名稱和圖標,展示數據時提升用戶體驗", + "optionalReason": "只有在選項裡開啟自動獲取功能後才會收集" + }, + "clipboard": { + "name": "剪切板內容", + "usage": "在設置每日時限規則時,為了操作方便,會讀取剪切板內 URL", + "optionalReason": "需要用戶手動同意" + } + } + }, + "storage": { + "title": "如何處理這些數據?", + "p1": "我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。", + "p2": "不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。", + "p3": "我們只幫助您收集數據,但處置權一定在您。" + } + }, + "en": { + "title": "Privacy statement", + "alert": "In order to provide you with complete services, this extension will necessarily collect some of your personal data during use, see the following privacy statement for details.", + "scope": { + "title": "What data is collected?", + "cols": { + "name": "Content", + "usage": "Usage", + "required": "Required" + }, + "rows": { + "website": { + "name": "Website browsing history", + "usage": "Used to count browsing time and visits" + }, + "tab": { + "name": "Tab information", + "usage": "Used to automatically obtain the name and icon of the website, and improve user experience when displaying data", + "optionalReason": "Only if this function is enabled in the options" + }, + "clipboard": { + "name": "Clipboard content", + "usage": "When setting the daily time limit rule, for the convenience of operation, the URL in the clipboard will be read", + "optionalReason": "Only if user agreed" + } + } + }, + "storage": { + "title": "How to do with this data?", + "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.", + "p3": "We only help you collect data, but the right of disposal must be yours." + } + }, + "ja": { + "title": "プライバシーに関する声明", + "alert": "完全なサービスを提供するために、この拡張機能は使用中に必ず個人データの一部を収集します。詳細については、次のプライバシーに関する声明を参照してください。", + "scope": { + "title": "どのようなデータが収集されますか?", + "cols": { + "name": "収集データ", + "usage": "使用", + "required": "それは必要ですか" + }, + "rows": { + "website": { + "name": "ウェブサイトの閲覧履歴", + "usage": "閲覧時間と訪問をカウントするために使用されます" + }, + "tab": { + "name": "タブ情報", + "usage": "Web サイトの名前とアイコンを自動的に取得し、データを表示する際のユーザー エクスペリエンスを向上させるために使用されます", + "optionalReason": "この機能がオプションで有効になっている場合のみ" + }, + "clipboard": { + "name": "クリップボードの内容", + "usage": "毎日の時間制限ルールを設定すると、操作の便宜上、クリップボードの URL が読み込まれます", + "optionalReason": "ユーザーが同意した場合のみ" + } + } + }, + "storage": { + "title": "このデータをどうするか?", + "p1": "この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。", + "p2": "ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。", + "p3": "私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません." + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/privacy.ts b/src/i18n/message/guide/privacy.ts index be36a95d7..db30f2f5a 100644 --- a/src/i18n/message/guide/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './privacy-resource.json' + type _StoreKey = | 'title' | 'p1' @@ -44,143 +46,6 @@ export type PrivacyMessage = { storage: { [key in _StoreKey]: string } } -const _default: Messages = { - zh_CN: { - title: '隐私声明', - alert: '为了向您提供完整的服务,该扩展在使用过程中会必要地收集您的一些个人数据,详情见以下隐私声明。', - scope: { - title: '哪些数据会被收集?', - cols: { - name: '内容', - usage: '用途', - required: '是否必需', - }, - rows: { - website: { - name: '网站访问记录', - usage: '用于统计浏览时长和访问次数', - }, - tab: { - name: '浏览器标签信息', - usage: '用于自动获取网站的名称和图标,展示数据时提升用户体验', - optionalReason: '只有在选项里开启自动获取功能后才会收集', - }, - clipboard: { - name: '剪切板内容', - usage: '在设置每日时限规则时,为了操作方便,会读取剪切板内 URL', - optionalReason: '需要用户手动同意' - }, - }, - }, - storage: { - title: '如何处理这些数据?', - p1: '我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。', - p2: '不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。', - p3: '我们只帮助您收集数据,但处置权一定在您。', - }, - }, - zh_TW: { - title: '隱私聲明', - alert: '為了向您提供完整的服務,該擴展在使用過程中會必要地收集您的一些個人數據,詳情見以下隱私聲明。', - scope: { - title: '哪些數據會被收集?', - cols: { - name: '內容', - usage: '用途', - required: '是否必需', - }, - rows: { - website: { - name: '網站訪問記錄', - usage: '用於統計瀏覽時長和訪問次數', - }, - tab: { - name: '瀏覽器標籤信息', - usage: '用於自動獲取網站的名稱和圖標,展示數據時提升用戶體驗', - optionalReason: '只有在選項裡開啟自動獲取功能後才會收集', - }, - clipboard: { - name: '剪切板內容', - usage: '在設置每日時限規則時,為了操作方便,會讀取剪切板內 URL', - optionalReason: '需要用戶手動同意' - }, - }, - }, - storage: { - title: '如何處理這些數據?', - p1: '我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。', - p2: '不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。', - p3: '我們只幫助您收集數據,但處置權一定在您。', - }, - }, - en: { - title: 'Privacy statement', - alert: 'In order to provide you with complete services, this extension will necessarily collect some of your personal data during use, see the following privacy statement for details.', - scope: { - title: 'What data is collected?', - cols: { - name: 'Content', - usage: 'Usage', - required: 'Required', - }, - rows: { - website: { - name: 'Website browsing history', - usage: 'Used to count browsing time and visits', - }, - tab: { - name: 'Tab information', - usage: 'Used to automatically obtain the name and icon of the website, and improve user experience when displaying data', - optionalReason: 'Only if this function is enabled in the options', - }, - clipboard: { - name: 'Clipboard content', - usage: 'When setting the daily time limit rule, for the convenience of operation, the URL in the clipboard will be read', - optionalReason: 'Only if user agreed' - }, - }, - }, - storage: { - title: 'How to do with this data?', - 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.', - p3: 'We only help you collect data, but the right of disposal must be yours.', - }, - }, - ja: { - title: 'プライバシーに関する声明', - alert: '完全なサービスを提供するために、この拡張機能は使用中に必ず個人データの一部を収集します。詳細については、次のプライバシーに関する声明を参照してください。', - scope: { - title: 'どのようなデータが収集されますか?', - cols: { - name: '収集データ', - usage: '使用', - required: 'それは必要ですか', - }, - rows: { - website: { - name: 'ウェブサイトの閲覧履歴', - usage: '閲覧時間と訪問をカウントするために使用されます', - }, - tab: { - name: 'タブ情報', - usage: 'Web サイトの名前とアイコンを自動的に取得し、データを表示する際のユーザー エクスペリエンスを向上させるために使用されます', - optionalReason: 'この機能がオプションで有効になっている場合のみ', - }, - clipboard: { - name: 'クリップボードの内容', - usage: '毎日の時間制限ルールを設定すると、操作の便宜上、クリップボードの URL が読み込まれます', - optionalReason: 'ユーザーが同意した場合のみ' - }, - }, - }, - storage: { - title: 'このデータをどうするか?', - p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', - p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', - p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', - }, - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/start-resource.json b/src/i18n/message/guide/start-resource.json new file mode 100644 index 000000000..aa4f0de37 --- /dev/null +++ b/src/i18n/message/guide/start-resource.json @@ -0,0 +1,46 @@ +{ + "zh_CN": { + "title": "快速开始", + "p1": "只需简单三步,就可以快速开始使用这个扩展。", + "s1": "1. 固定扩展的图标", + "s1p1": "首先,为了更方便地使用这个扩展,你需要将图标固定到工具栏上。不同浏览器的操作方式不同,下图显示了在 Chrome 中的做法。", + "s2": "2. 浏览任何网站", + "s2p1": "然后你可以点开任何网站,扩展图标上会展示该网站的当日浏览时间,就像这样 {demo}。", + "s3": "3. 在弹出页中查看数据", + "s3p1": "最后,点击扩展的图标,你可以在弹出的页面里看到今天、本周以及本月的饼状图数据。", + "alert": "你已经学会了基本用法,赶紧试试!!" + }, + "en": { + "title": "Get started", + "p1": "You can quickly start using this extension in just 3 easy steps.", + "s1": "1. Pin the icon", + "s1p1": "Firstly, to use this extension more conveniently, you'd better pin the icon to toolbar. It's not the same in different browsers to do this, the following figure shows how in Chrome.", + "s2": "2. Browse any website", + "s2p1": "Then, browse any website, and you will see that the time is beating on the icon, just like this {demo}.", + "s3": "3. Read data in the popup page", + "s3p1": "Finally, click the icon to open the popup page, and you can read the data visualized with pie chart, of today, this week or this month.", + "alert": "You have learned the basic usage, try it!!" + }, + "zh_TW": { + "title": "快速開始", + "p1": "只需簡單三步,就可以快速開始使用這個擴展。", + "s1": "1. 固定擴展的圖標", + "s1p1": "首先,為了更方便地使用這個擴展,你需要將圖標固定到工具欄上。不同瀏覽器的操作方式不同,下圖顯示了在 Chrome 中的做法。", + "s2": "2. 瀏覽任何網站", + "s2p1": "然後你可以點開任何網站,擴展圖標上會展示該網站的當日瀏覽時間,就像這樣 {demo}。", + "s3": "3. 在彈出頁中查看數據", + "s3p1": "最後,點擊擴展的圖標,你可以在彈出的頁面裡看到今天、本週以及本月的餅狀圖數據。", + "alert": "你已經學會了基本用法,趕緊試試!!" + }, + "ja": { + "title": "始めましょう", + "p1": "この拡張機能は、わずか 3 つの簡単な手順ですぐに使い始めることができます。", + "s1": "1. アイコンを固定する", + "s1p1": "まず、この拡張機能をより便利に使用するには、アイコンをツールバーにピン留めすることをお勧めします。 これを行う方法はブラウザーによって異なります。次の図は、Chrome での方法を示しています。", + "s2": "2. 任意の Web サイトを閲覧する", + "s2p1": "次に、任意の Web サイトを閲覧すると、この {demo} のように、時間がアイコンに刻み込まれていることがわかります。", + "s3": "3. ポップアップ ページでデータを読み取る", + "s3p1": "最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。", + "alert": "基本的な使い方を学んだので、試してみてください!!" + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/start.ts b/src/i18n/message/guide/start.ts index 1c180f2a1..4e00c6fd8 100644 --- a/src/i18n/message/guide/start.ts +++ b/src/i18n/message/guide/start.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './start-resource.json' + type _Key = | 'p1' | 's1' @@ -21,51 +23,6 @@ export type StartMessage = { [key in _Key]: string } -const _default: Messages = { - zh_CN: { - title: '快速开始', - p1: '只需简单三步,就可以快速开始使用这个扩展。', - s1: '1. 固定扩展的图标', - s1p1: '首先,为了更方便地使用这个扩展,你需要将图标固定到工具栏上。不同浏览器的操作方式不同,下图显示了在 Chrome 中的做法。', - s2: '2. 浏览任何网站', - s2p1: '然后你可以点开任何网站,扩展图标上会展示该网站的当日浏览时间,就像这样 {demo}。', - s3: '3. 在弹出页中查看数据', - s3p1: '最后,点击扩展的图标,你可以在弹出的页面里看到今天、本周以及本月的饼状图数据。', - alert: '你已经学会了基本用法,赶紧试试!!', - }, - en: { - title: 'Get started', - p1: 'You can quickly start using this extension in just 3 easy steps.', - s1: '1. Pin the icon', - s1p1: 'Firstly, to use this extension more conveniently, you\'d better pin the icon to toolbar. It\'s not the same in different browsers to do this, the following figure shows how in Chrome.', - s2: '2. Browse any website', - s2p1: 'Then, browse any website, and you will see that the time is beating on the icon, just like this {demo}.', - s3: '3. Read data in the popup page', - s3p1: 'Finally, click the icon to open the popup page, and you can read the data visualized with pie chart, of today, this week or this month.', - alert: 'You have learned the basic usage, try it!!', - }, - zh_TW: { - title: '快速開始', - p1: '只需簡單三步,就可以快速開始使用這個擴展。', - s1: '1. 固定擴展的圖標', - s1p1: '首先,為了更方便地使用這個擴展,你需要將圖標固定到工具欄上。不同瀏覽器的操作方式不同,下圖顯示了在 Chrome 中的做法。', - s2: '2. 瀏覽任何網站', - s2p1: '然後你可以點開任何網站,擴展圖標上會展示該網站的當日瀏覽時間,就像這樣 {demo}。', - s3: '3. 在彈出頁中查看數據', - s3p1: '最後,點擊擴展的圖標,你可以在彈出的頁面裡看到今天、本週以及本月的餅狀圖數據。', - alert: '你已經學會了基本用法,趕緊試試!!', - }, - ja: { - title: '始めましょう', - p1: 'この拡張機能は、わずか 3 つの簡単な手順ですぐに使い始めることができます。', - s1: '1. アイコンを固定する', - s1p1: 'まず、この拡張機能をより便利に使用するには、アイコンをツールバーにピン留めすることをお勧めします。 これを行う方法はブラウザーによって異なります。次の図は、Chrome での方法を示しています。', - s2: '2. 任意の Web サイトを閲覧する', - s2p1: '次に、任意の Web サイトを閲覧すると、この {demo} のように、時間がアイコンに刻み込まれていることがわかります。', - s3: '3. ポップアップ ページでデータを読み取る', - s3p1: '最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。', - alert: '基本的な使い方を学んだので、試してみてください!!', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/virtual-resource.json b/src/i18n/message/guide/virtual-resource.json new file mode 100644 index 000000000..5b58e806e --- /dev/null +++ b/src/i18n/message/guide/virtual-resource.json @@ -0,0 +1,46 @@ +{ + "en": { + "title": "Count data for specific URLs", + "p1": "If you want to count the browsing time and number of visits of certain URLs, not just the domain name, you can do so by creating a virtual site.", + "step": { + "title": "Four steps to create a virtual site", + "enter": "First, enter the management page{link}, click the menu item {menuItem}.", + "click": "Click the New button in the upper right corner.", + "form": "Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, and then click Save.", + "browse": "Finally, Browse the relevant URLs and observe the data." + } + }, + "zh_CN": { + "title": "统计特定 URL 的数据", + "p1": "如果你想统计某些 URL ,而不只是域名的浏览时长和访问次数,可以通过创建虚拟站点来实现。", + "step": { + "title": "简单四步创建一个虚拟站点", + "enter": "首先进入管理页{link},点击菜单项【{menuItem}】。", + "click": "然后单击右上角的新建按钮。", + "form": "填写要统计的 URL,可以使用 Ant 表达式,如 {demo1} 或 {demo2} ,以及这个虚拟站点的名称,然后点击保存。", + "browse": "最后浏览相关的网页,观察数据。" + } + }, + "zh_TW": { + "title": "統計特定 URL 的數據", + "p1": "如果你想統計某些 URL ,而不只是域名的瀏覽時長和訪問次數,可以通過創建虛擬站點來實現。", + "step": { + "title": "簡單四步創建一個虛擬站點", + "enter": "首先進入管理頁{link},點擊菜單項【{menuItem}】。", + "click": "然後單擊右上角的新建按鈕。", + "form": "填寫要統計的 URL,可以使用 Ant 表達式,如 {demo1} 或 {demo2} ,以及這個虛擬站點的名稱,然後點擊保存。", + "browse": "最後瀏覽相關的網頁,觀察數據。" + } + }, + "ja": { + "title": "特定の URL のデータをカウントする", + "p1": "ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。", + "step": { + "title": "仮想サイトを作成するための 4 つのステップ", + "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "click": "右上隅にある [新規] ボタンをクリックします。", + "form": "次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。", + "browse": "最後に、関連する URL を参照してデータを観察します。" + } + } +} \ No newline at end of file diff --git a/src/i18n/message/guide/virtual.ts b/src/i18n/message/guide/virtual.ts index 30eaa5945..5fab3fe08 100644 --- a/src/i18n/message/guide/virtual.ts +++ b/src/i18n/message/guide/virtual.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './virtual-resource.json' + export type VirtualMessage = { title: string p1: string @@ -17,53 +19,6 @@ export type VirtualMessage = { } } -const _default: Messages = { - en: { - title: 'Count data for specific URLs', - p1: 'If you want to count the browsing time and number of visits of certain URLs, not just the domain name, ' - + 'you can do so by creating a virtual site.', - step: { - title: 'Four steps to create a virtual site', - enter: 'First, enter the management page{link}, click the menu item {menuItem}.', - click: 'Click the New button in the upper right corner.', - form: 'Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, ' - + 'and then click Save.', - browse: 'Finally, Browse the relevant URLs and observe the data.', - } - }, - zh_CN: { - title: '统计特定 URL 的数据', - p1: '如果你想统计某些 URL ,而不只是域名的浏览时长和访问次数,可以通过创建虚拟站点来实现。', - step: { - title: '简单四步创建一个虚拟站点', - enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', - click: '然后单击右上角的新建按钮。', - form: '填写要统计的 URL,可以使用 Ant 表达式,如 {demo1} 或 {demo2} ,以及这个虚拟站点的名称,然后点击保存。', - browse: '最后浏览相关的网页,观察数据。' - } - }, - zh_TW: { - title: '統計特定 URL 的數據', - p1: '如果你想統計某些 URL ,而不只是域名的瀏覽時長和訪問次數,可以通過創建虛擬站點來實現。', - step: { - title: '簡單四步創建一個虛擬站點', - enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', - click: '然後單擊右上角的新建按鈕。', - form: '填寫要統計的 URL,可以使用 Ant 表達式,如 {demo1} 或 {demo2} ,以及這個虛擬站點的名稱,然後點擊保存。', - browse: '最後瀏覽相關的網頁,觀察數據。', - }, - }, - ja: { - title: '特定の URL のデータをカウントする', - p1: 'ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。', - step: { - title: '仮想サイトを作成するための 4 つのステップ', - enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', - click: '右上隅にある [新規] ボタンをクリックします。', - form: '次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。', - browse: '最後に、関連する URL を参照してデータを観察します。', - } - }, -} +const _default: Messages = resource export default _default diff --git a/src/i18n/message/popup/chart-resource.json b/src/i18n/message/popup/chart-resource.json new file mode 100644 index 000000000..b8a68d6f8 --- /dev/null +++ b/src/i18n/message/popup/chart-resource.json @@ -0,0 +1,86 @@ +{ + "zh_CN": { + "title": { + "today": "今日数据", + "thisWeek": "本周数据", + "thisMonth": "本月数据", + "last30Days": "近 30 天数据" + }, + "mergeHostLabel": "合并子域名", + "fileName": "上网时长清单_{today}_by_{app}", + "saveAsImageTitle": "保存", + "restoreTitle": "刷新", + "options": "设置", + "totalTime": "共 {totalTime}", + "totalCount": "共 {totalCount} 次", + "averageTime": "平均每天 {value}", + "averageCount": "平均每天 {value} 次", + "otherLabel": "其他{count}个网站", + "updateVersion": "版本升级", + "updateVersionInfo": "最新版本:{version}", + "updateVersionInfo4Firefox": "新版本 {version} 已发布\n\n您可以前往插件管理页进行更新" + }, + "zh_TW": { + "title": { + "today": "今日數據", + "thisWeek": "本週數據", + "thisMonth": "本月數據", + "last30Days": "近 30 天數據" + }, + "mergeHostLabel": "合並子網域", + "fileName": "上網時長清單_{today}_by_{app}", + "saveAsImageTitle": "保存", + "restoreTitle": "刷新", + "options": "設置", + "totalTime": "共 {totalTime}", + "totalCount": "共 {totalCount} 次", + "averageCount": "平均每天 {value} 次", + "averageTime": "平均每天 {value}", + "otherLabel": "其他{count}個站點", + "updateVersion": "版本昇級", + "updateVersionInfo": "最新版本:{version}", + "updateVersionInfo4Firefox": "新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新" + }, + "en": { + "title": { + "today": "Today's Data", + "thisWeek": "This Week's Data", + "thisMonth": "This Month's Data", + "last30Days": "Last 30 days' data" + }, + "mergeHostLabel": "Merge Sites", + "fileName": "Web_Time_List_{today}_By_{app}", + "saveAsImageTitle": "Snapshot", + "restoreTitle": "Restore", + "options": "Options", + "totalTime": "Total {totalTime}", + "totalCount": "Total {totalCount} times", + "averageCount": "{value} times per day on average", + "averageTime": "{value} per day on average", + "otherLabel": "Other {count} sites", + "updateVersion": "Updatable", + "updateVersionInfo": "Latest: {version}", + "updateVersionInfo4Firefox": "Upgrade to {version} in the management page, about:addons, pls" + }, + "ja": { + "title": { + "today": "今日のデータ", + "thisWeek": "今週のデータ", + "thisMonth": "今月のデータ", + "last30Days": "過去 30 日間のデータ" + }, + "mergeHostLabel": "URLをマージ", + "fileName": "オンライン時間_{today}_by_{app}", + "saveAsImageTitle": "ダウンロード", + "restoreTitle": "刷新", + "options": "設定", + "totalTime": "合計 {totalTime}", + "totalCount": "合計 {totalCount} 回", + "averageTime": "1日平均 {value}", + "averageCount": "1日平均 {value} 回", + "otherLabel": "他{count}サイト", + "updateVersion": "更新", + "updateVersionInfo": "最新バージョン:{version}", + "updateVersionInfo4Firefox": "管理ページで {version} にアップグレードしてください" + } +} \ No newline at end of file diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index ec117a37d..de52ec824 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +import resource from './chart-resource.json' + export type ChartMessage = { title: { [key in PopupDuration]: string } mergeHostLabel: string @@ -22,92 +24,7 @@ export type ChartMessage = { updateVersionInfo4Firefox: string } -const _default: Messages = { - zh_CN: { - title: { - today: '今日数据', - thisWeek: '本周数据', - thisMonth: '本月数据', - last30Days: '近 30 天数据', - }, - mergeHostLabel: '合并子域名', - fileName: '上网时长清单_{today}_by_{app}', - saveAsImageTitle: '保存', - restoreTitle: '刷新', - options: '设置', - totalTime: '共 {totalTime}', - totalCount: '共 {totalCount} 次', - averageTime: '平均每天 {value}', - averageCount: '平均每天 {value} 次', - otherLabel: '其他{count}个网站', - updateVersion: '版本升级', - updateVersionInfo: '最新版本:{version}', - updateVersionInfo4Firefox: '新版本 {version} 已发布\n\n您可以前往插件管理页进行更新', - }, - zh_TW: { - title: { - today: '今日數據', - thisWeek: '本週數據', - thisMonth: '本月數據', - last30Days: '近 30 天數據', - }, - mergeHostLabel: '合並子網域', - fileName: '上網時長清單_{today}_by_{app}', - saveAsImageTitle: '保存', - restoreTitle: '刷新', - options: '設置', - totalTime: '共 {totalTime}', - totalCount: '共 {totalCount} 次', - averageCount: '平均每天 {value} 次', - averageTime: '平均每天 {value}', - otherLabel: '其他{count}個站點', - updateVersion: '版本昇級', - updateVersionInfo: '最新版本:{version}', - updateVersionInfo4Firefox: '新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新', - }, - en: { - title: { - today: 'Today\'s Data', - thisWeek: 'This Week\'s Data', - thisMonth: 'This Month\'s Data', - last30Days: 'Last 30 days\' data', - }, - mergeHostLabel: 'Merge Sites', - fileName: 'Web_Time_List_{today}_By_{app}', - saveAsImageTitle: 'Snapshot', - restoreTitle: 'Restore', - options: 'Options', - totalTime: 'Total {totalTime}', - totalCount: 'Total {totalCount} times', - averageCount: '{value} times per day on average', - averageTime: '{value} per day on average', - otherLabel: 'Other {count} sites', - updateVersion: 'Updatable', - updateVersionInfo: 'Latest: {version}', - updateVersionInfo4Firefox: 'Upgrade to {version} in the management page, about:addons, pls', - }, - ja: { - title: { - today: '今日のデータ', - thisWeek: '今週のデータ', - thisMonth: '今月のデータ', - last30Days: '過去 30 日間のデータ', - }, - mergeHostLabel: 'URLをマージ', - fileName: 'オンライン時間_{today}_by_{app}', - saveAsImageTitle: 'ダウンロード', - restoreTitle: '刷新', - options: '設定', - totalTime: '合計 {totalTime}', - totalCount: '合計 {totalCount} 回', - averageTime: '1日平均 {value}', - averageCount: '1日平均 {value} 回', - otherLabel: '他{count}サイト', - updateVersion: '更新', - updateVersionInfo: '最新バージョン:{version}', - updateVersionInfo4Firefox: '管理ページで {version} にアップグレードしてください', - }, -} +const _default: Messages = resource export default _default \ No newline at end of file From caec82647e227d14e9cff60f88fbd81b94036185 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 16 May 2023 00:02:20 +0800 Subject: [PATCH 151/168] Fix: set -> sep --- src/i18n/message/common/calendar-resource.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index b79ebc8cd..9c8630ecf 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -13,13 +13,13 @@ }, "en": { "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", - "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", "dateFormat": "{m}/{d}/{y}", "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}" }, "ja": { "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", - "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", "dateFormat": "{y}/{m}/{d}", "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" } From a0bb0f92f04dff26af081df7fd354c8a2e8a4885 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 16 May 2023 00:28:03 +0800 Subject: [PATCH 152/168] Fix translations of English --- src/content-script/printer.ts | 3 ++- src/i18n/message/common/content-script-resource.json | 2 +- src/i18n/message/common/popup-duration-resource.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/content-script/printer.ts b/src/content-script/printer.ts index 803ea1e02..55834a9ee 100644 --- a/src/content-script/printer.ts +++ b/src/content-script/printer.ts @@ -8,6 +8,7 @@ import { t } from "./locale" import { formatPeriod } from "@util/time" import { sendMsg2Runtime } from "@api/chrome/runtime" +import { t2Chrome } from "@i18n/chrome/t" /** * Print info of today @@ -24,7 +25,7 @@ export default async function printInfo(host: string) { .replace('{time}', waste.time ? '' + waste.time : '-') .replace('{focus}', formatPeriod(waste.focus, msg)) .replace('{host}', host) - const info1 = t(msg => msg.closeAlert) + const info1 = t(msg => msg.closeAlert).replace('{appName}', t2Chrome(msg => msg.meta.name)) console.log(info0) console.log(info1) } \ No newline at end of file diff --git a/src/i18n/message/common/content-script-resource.json b/src/i18n/message/common/content-script-resource.json index fb4bd3117..6d16de4d2 100644 --- a/src/i18n/message/common/content-script-resource.json +++ b/src/i18n/message/common/content-script-resource.json @@ -19,7 +19,7 @@ }, "en": { "consoleLog": "You have open {host} for {time} time(s) and browsed it for {focus} today.", - "closeAlert": "You can turn off the above tips in the option of Timer!", + "closeAlert": "You can turn off the above tips in the option of [{appName}]!", "timeWithHour": "{hour} hour(s) {minute} minute(s) {second} second(s)", "timeWithMinute": "{minute} minute(s) {second} second(s)", "timeWithSecond": "{second} second(s)", diff --git a/src/i18n/message/common/popup-duration-resource.json b/src/i18n/message/common/popup-duration-resource.json index 3799c8dd7..0ede39dfe 100644 --- a/src/i18n/message/common/popup-duration-resource.json +++ b/src/i18n/message/common/popup-duration-resource.json @@ -15,7 +15,7 @@ "today": "Today's", "thisWeek": "This Week's", "thisMonth": "This Month's", - "last30Days": "Last 30 days'" + "last30Days": "Last 30 Days'" }, "ja": { "today": "今日の", From 93137a8f9ca11ce3e7ef0741f388f8b55a2f49fb Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 16 May 2023 22:17:18 +0800 Subject: [PATCH 153/168] Optimize translations (#212) --- .../option/components/backup/index.ts | 19 +++++++++++++++---- src/guide/component/home/index.ts | 2 +- src/i18n/message/app/option-resource.json | 11 +---------- src/i18n/message/app/option.ts | 3 +-- src/i18n/message/app/report-resource.json | 2 +- src/i18n/message/common/meta-resource.json | 2 +- src/i18n/message/common/meta.ts | 2 -- src/i18n/message/guide/home-resource.json | 8 ++++---- src/i18n/message/guide/virtual-resource.json | 2 +- 9 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index ffcee3c0f..bb64c1fcd 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -5,7 +5,8 @@ * https://opensource.org/licenses/MIT */ -import { Ref } from "vue" +import type { Ref } from "vue" +import type { I18nKey } from "@app/locale" import { t } from "@app/locale" import optionService from "@service/option-service" @@ -23,9 +24,19 @@ const ALL_TYPES: timer.backup.Type[] = [ 'gist', ] +const AUTH_LABELS: { [t in timer.backup.Type]: string } = { + 'none': '', + 'gist': 'Personal Access Token {info} {input}', +} + +const TYPE_NAMES: { [t in timer.backup.Type]: I18nKey } = { + none: msg => msg.option.backup.meta.none.label, + gist: _ => 'GitHub Gist', +} + const typeOptions = () => ALL_TYPES.map(type => h(ElOption, { value: type, - label: t(msg => msg.option.backup.meta[type].label) + label: t(TYPE_NAMES[type]), })) const typeSelect = (type: Ref, handleChange?: Function) => h(ElSelect, @@ -136,7 +147,7 @@ const _default = defineComponent({ input: typeSelect(type, handleChange) }, msg => msg.backup.type, - t(msg => msg.option.backup.meta[DEFAULT.backupType].label) + t(TYPE_NAMES[DEFAULT.backupType]) ) ] type.value !== 'none' && nodes.push( @@ -157,7 +168,7 @@ const _default = defineComponent({ input: authInput(auth, handleChange, handleTest), info: tooltip(msg => msg.option.backup.meta[type.value]?.authInfo) }, - msg => msg.backup.meta[type.value]?.auth + _msg => AUTH_LABELS[type.value], ), h(ElDivider), renderOptionItem({ diff --git a/src/guide/component/home/index.ts b/src/guide/component/home/index.ts index 3dbca1bae..83a789d5e 100644 --- a/src/guide/component/home/index.ts +++ b/src/guide/component/home/index.ts @@ -17,7 +17,7 @@ const _default = defineComponent(() => { return () => h('div', { class: 'home-container' }, [ h('h1', { class: 'slogan' }, t(msg => msg.meta.slogan)), h('img', { src: PIC_URL }), - h('p', { class: 'desc' }, t(msg => msg.home.desc)), + h('p', { class: 'desc' }, t(msg => msg.home.desc, { appName: t(msg => msg.meta.name) })), h('div', { class: 'button-container' }, [ h(StartButton), h(DownloadButton) diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index 66a661ea3..d451c884e 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -64,12 +64,9 @@ "client": "客户端标识 {input}", "meta": { "none": { - "label": "不开启备份", - "auth": "" + "label": "不开启备份" }, "gist": { - "label": "GitHub Gist", - "auth": "Personal Access Token {info} {input}", "authInfo": "需要创建一个至少包含 gist 权限的 token" } }, @@ -154,8 +151,6 @@ "label": "關閉備份" }, "gist": { - "label": "GitHub Gist", - "auth": "Personal Access Token {info} {input}", "authInfo": "需要創建一個至少包含 gist 權限的 token" } }, @@ -240,8 +235,6 @@ "label": "Always off" }, "gist": { - "label": "GitHub Gist", - "auth": "Personal Access Token {info} {input}", "authInfo": "One token with at least gist permission is required" } }, @@ -326,8 +319,6 @@ "label": "バックアップを有効にしない" }, "gist": { - "label": "GitHub Gist", - "auth": "Personal Access Token {info} {input}", "authInfo": "少なくとも gist 権限を持つトークンが 1 つ必要です" } }, diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 43273ebea..05c370c6c 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -67,8 +67,7 @@ export type OptionMessage = { client: string meta: { [type in timer.backup.Type]: { - label: string - auth?: string + label?: string authInfo?: string } } diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index bd79f4531..82fd5022e 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -73,7 +73,7 @@ "mergeDate": "Merge date", "mergeDomain": "Merge URL", "hostPlaceholder": "Partial URL, then enter", - "exportFileName": "Timer_Data", + "exportFileName": "My_Browsing_Time", "added2Whitelist": "Added into the whitelist", "removeFromWhitelist": "Removed from the whitelist", "batchDelete": { diff --git a/src/i18n/message/common/meta-resource.json b/src/i18n/message/common/meta-resource.json index 23a6018f5..7ddb027bd 100644 --- a/src/i18n/message/common/meta-resource.json +++ b/src/i18n/message/common/meta-resource.json @@ -12,7 +12,7 @@ }, "ja": { "name": "Web時間統計", - "marketName": "ウェブタイムトラッカー", + "marketName": "Web時間統計", "description": "最高のウェブタイムトラッカーになるために。" }, "en": { diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index 35f5b94e3..89d9d29e3 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -14,8 +14,6 @@ export type MetaMessage = { slogan: string } -const SLOGAN_EN = 'Insight & Improve' - const _default: Messages = resource export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/home-resource.json b/src/i18n/message/guide/home-resource.json index 55563ccb3..1d7be3e20 100644 --- a/src/i18n/message/guide/home-resource.json +++ b/src/i18n/message/guide/home-resource.json @@ -1,21 +1,21 @@ { "en": { - "desc": "Timer can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.", + "desc": "{appName} can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.", "button": "Start Now!", "download": "Install for {browser}" }, "zh_CN": { - "desc": "网费很贵是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。", + "desc": "{appName}是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。", "button": "如何使用", "download": "在 {browser} 上安装" }, "zh_TW": { - "desc": "網費很貴是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。", + "desc": "{appName}是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。", "button": "如何使用", "download": "在 {browser} 上安裝" }, "ja": { - "desc": "この拡張機能は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ち、ウェブ習慣を洞察して改善することができます.", + "desc": "{appName}は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ちます。これにより、ウェブ習慣を洞察して改善することができます。", "button": "すぐに始めましょう", "download": "{browser} にインストール" } diff --git a/src/i18n/message/guide/virtual-resource.json b/src/i18n/message/guide/virtual-resource.json index 5b58e806e..4b02f4e7b 100644 --- a/src/i18n/message/guide/virtual-resource.json +++ b/src/i18n/message/guide/virtual-resource.json @@ -7,7 +7,7 @@ "enter": "First, enter the management page{link}, click the menu item {menuItem}.", "click": "Click the New button in the upper right corner.", "form": "Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, and then click Save.", - "browse": "Finally, Browse the relevant URLs and observe the data." + "browse": "Finally, browse the relevant URLs and observe the data." } }, "zh_CN": { From c45ab5bedb9b9b9e046cf2ce644062111ec1e14d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 16 May 2023 23:18:52 +0800 Subject: [PATCH 154/168] Support Portuguese (#212) --- script/crowdin/common.ts | 7 +- script/crowdin/crowdin.d.ts | 3 +- src/app/components/help-us/progress-list.ts | 1 - .../option/components/appearance/index.ts | 6 +- .../report/table/columns/operation.ts | 1 + src/app/index.ts | 3 +- src/guide/index.ts | 3 +- src/guide/layout/header/locale-select.ts | 3 +- src/i18n/chrome/index.ts | 3 +- src/i18n/chrome/message.ts | 6 ++ src/i18n/index.ts | 7 +- src/i18n/message/app/analysis-resource.json | 39 +++++++- src/i18n/message/app/confirm-resource.json | 4 + src/i18n/message/app/dashboard-resource.json | 26 +++++ .../message/app/data-manage-resource.json | 24 +++++ src/i18n/message/app/habit-resource.json | 25 +++++ src/i18n/message/app/index.ts | 23 ++++- src/i18n/message/app/limit-resource.json | 40 +++++++- src/i18n/message/app/menu-resource.json | 20 ++++ src/i18n/message/app/merge-rule-resource.json | 17 +++- src/i18n/message/app/operation-resource.json | 6 ++ src/i18n/message/app/option-resource.json | 97 +++++++++++++++++-- src/i18n/message/app/report-resource.json | 32 ++++++ .../message/app/site-manage-resource.json | 50 +++++++++- .../message/app/time-format-resource.json | 6 ++ src/i18n/message/app/whitelist-resource.json | 10 ++ src/i18n/message/common/base-resource.json | 5 + .../message/common/calendar-resource.json | 8 +- .../common/content-script-resource.json | 13 ++- .../common/context-menus-resource.json | 11 ++- src/i18n/message/common/initial-resource.json | 8 ++ src/i18n/message/common/item-resource.json | 18 ++++ src/i18n/message/common/locale-resource.json | 10 +- src/i18n/message/common/merge-resource.json | 6 ++ src/i18n/message/common/meta-resource.json | 5 + .../common/popup-duration-resource.json | 6 ++ src/i18n/message/guide/app-resource.json | 7 ++ src/i18n/message/guide/backup-resource.json | 25 ++++- src/i18n/message/guide/home-resource.json | 5 + src/i18n/message/guide/index.ts | 15 +++ src/i18n/message/guide/layout-resource.json | 9 ++ src/i18n/message/guide/limit-resource.json | 13 ++- src/i18n/message/guide/merge-resource.json | 34 ++++++- src/i18n/message/guide/privacy-resource.json | 34 +++++++ src/i18n/message/guide/start-resource.json | 11 +++ src/i18n/message/guide/virtual-resource.json | 13 ++- src/i18n/message/popup/chart-resource.json | 25 ++++- src/i18n/message/popup/index.ts | 7 ++ src/util/constant/url.ts | 3 +- types/timer/index.d.ts | 4 +- 50 files changed, 707 insertions(+), 50 deletions(-) diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 5e64b7f28..cb6614a93 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -11,13 +11,15 @@ export const SOURCE_LOCALE: timer.SourceLocale = 'en' export const ALL_TRANS_LOCALES: timer.Locale[] = [ 'ja', 'zh_TW', + 'pt', ] const CROWDIN_I18N_MAP: Record = { en: 'en', ja: 'ja', - "zh-CN": 'zh_CN', - "zh-TW": 'zh_TW', + 'zh-CN': 'zh_CN', + 'zh-TW': 'zh_TW', + 'pt-PT': 'pt', } const I18N_CROWDIN_MAP: Record = { @@ -25,6 +27,7 @@ const I18N_CROWDIN_MAP: Record = { ja: 'ja', zh_CN: 'zh-CN', zh_TW: 'zh-TW', + pt: 'pt-PT' } export const crowdinLangOf = (locale: timer.Locale) => I18N_CROWDIN_MAP[locale] diff --git a/script/crowdin/crowdin.d.ts b/script/crowdin/crowdin.d.ts index e887914e6..589f2cc9f 100644 --- a/script/crowdin/crowdin.d.ts +++ b/script/crowdin/crowdin.d.ts @@ -36,4 +36,5 @@ type CrowdinLanguage = | 'zh-CN' | 'en' | 'zh-TW' - | 'ja' \ No newline at end of file + | 'ja' + | 'pt-PT' \ No newline at end of file diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index a84dc1cbf..beb02d681 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -23,7 +23,6 @@ const localeCrowdMap: { [locale in SupportedLocale]: string } = { ko: "ko", pl: "pl", pt: "pt-PT", - pt_BR: "pt-BR", ru: "ru", uk: "uk", fr: "fr", diff --git a/src/app/components/option/components/appearance/index.ts b/src/app/components/option/components/appearance/index.ts index b97b9d672..6096dabe6 100644 --- a/src/app/components/option/components/appearance/index.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -15,7 +15,7 @@ import DarkModeInput from "./dark-mode-input" import { t, tWith } from "@app/locale" import { renderOptionItem, tagText } from "../../common" import localeMessages from "@i18n/message/common/locale" -import { localeSameAsBrowser } from "@i18n" +import { ALL_LOCALES, localeSameAsBrowser } from "@i18n" import { toggle } from "@util/dark-mode" const displayWhitelist = (option: UnwrapRef) => h(ElSwitch, { @@ -42,10 +42,10 @@ const printInConsole = (option: UnwrapRef) => h(E } }) -const allLocales: timer.Locale[] = (["zh_CN", "zh_TW", "en", "ja"] as timer.Locale[]) +const SORTED_LOCALES: timer.Locale[] = ALL_LOCALES // Keep the locale same as this browser first position .sort((a, _b) => a === localeSameAsBrowser ? -1 : 0) -const allLocaleOptions: timer.option.LocaleOption[] = ["default", ...allLocales] +const allLocaleOptions: timer.option.LocaleOption[] = ["default", ...SORTED_LOCALES] const locale = (option: UnwrapRef) => h(ElSelect, { modelValue: option.locale, diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index db456a8f7..4f396f2f2 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -48,6 +48,7 @@ const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { zh_CN: 290, ja: 360, zh_TW: 290, + pt: 400, } const _default = defineComponent({ name: "OperationColumn", diff --git a/src/app/index.ts b/src/app/index.ts index 55b049925..e53ef4e7a 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -24,7 +24,8 @@ const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), en: () => import('element-plus/lib/locale/lang/en'), - ja: () => import('element-plus/lib/locale/lang/ja') + ja: () => import('element-plus/lib/locale/lang/ja'), + pt: () => import('element-plus/lib/locale/lang/pt'), } async function main() { diff --git a/src/guide/index.ts b/src/guide/index.ts index 839e4808b..9b30f67e9 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -19,7 +19,8 @@ const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), en: () => import('element-plus/lib/locale/lang/en'), - ja: () => import('element-plus/lib/locale/lang/ja') + ja: () => import('element-plus/lib/locale/lang/ja'), + pt: () => import('element-plus/lib/locale/lang/pt'), } async function main() { diff --git a/src/guide/layout/header/locale-select.ts b/src/guide/layout/header/locale-select.ts index d7cb5e97e..cfb381a2f 100644 --- a/src/guide/layout/header/locale-select.ts +++ b/src/guide/layout/header/locale-select.ts @@ -7,7 +7,7 @@ import { ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus" import { defineComponent, h } from "vue" -import { getI18nVal, locale as current } from "@i18n" +import { getI18nVal, locale as current, ALL_LOCALES } from "@i18n" import localeMessages from "@i18n/message/common/locale" import SvgIcon from "./svg-icon" import { LOCALE_PATH } from "./svg" @@ -16,7 +16,6 @@ import { createTabAfterCurrent } from "@api/chrome/tab" import { CROWDIN_HOMEPAGE } from "@util/constant/url" const HELP_CMD: string = '_help' -const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja'] const getLocaleName = (locale: timer.Locale) => getI18nVal(localeMessages, msg => msg.name, locale) diff --git a/src/i18n/chrome/index.ts b/src/i18n/chrome/index.ts index f655f9520..8b5ededef 100644 --- a/src/i18n/chrome/index.ts +++ b/src/i18n/chrome/index.ts @@ -17,7 +17,8 @@ const _default: { [locale in FakedLocale]: any } = { zh_CN: compile(messages.zh_CN), zh_TW: compile(messages.zh_TW), en: compile(messages.en), - ja: compile(messages.ja) + ja: compile(messages.ja), + pt: compile(messages.pt) } export default _default \ No newline at end of file diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index a3a653f6b..a3567a5d9 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -41,6 +41,12 @@ const messages: Messages = { base: baseMessages.ja, contextMenus: contextMenusMessages.ja, initial: initialMessages.ja, + }, + pt: { + meta: metaMessages.pt, + base: baseMessages.pt, + contextMenus: contextMenusMessages.pt, + initial: initialMessages.pt, } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index d23bf7190..fa829c835 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -16,15 +16,20 @@ export type FakedLocale = timer.Locale */ const FEEDBACK_LOCALE: timer.Locale = "en" +export const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja', 'pt'] + export const defaultLocale: timer.Locale = "zh_CN" // Standardize the locale code according to the Chrome locale code const chrome2I18n: { [key: string]: timer.Locale } = { 'zh-CN': "zh_CN", 'zh-TW': "zh_TW", + 'en': 'en', 'en-US': "en", 'en-GB': "en", 'ja': "ja", + 'pt-PT': 'pt', + 'pt-BR': 'pt', } const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { @@ -33,8 +38,6 @@ const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { "es-MX": 'es', ko: 'ko', pl: 'pl', - "pt-PT": 'pt', - "pt-BR": 'pt_BR', ru: 'ru', uk: 'uk', fr: 'fr', diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json index 073d8d7b3..4d80b3243 100644 --- a/src/i18n/message/app/analysis-resource.json +++ b/src/i18n/message/app/analysis-resource.json @@ -109,9 +109,9 @@ "emptyDesc": "サイトは空です" }, "summary": { - "title": "Summary", - "day": "Total active days", - "firstDay": "First visit {value}" + "title": "まとめ", + "day": "合計アクティブ日数", + "firstDay": "{value} を初めて訪問する" }, "trend": { "title": "レンジトレンド", @@ -130,5 +130,38 @@ "focusTitle": "タイム トレンドの閲覧", "visitTitle": "訪問数の傾向" } + }, + "pt": { + "common": { + "focusTotal": "Tempo total de navegação", + "visitTotal": "Total de visitas", + "ringGrowth": "{value} em comparação com o período anterior", + "merged": "Mesclado", + "virtual": "Virtual", + "hostPlaceholder": "Procure um site para analisar", + "emptyDesc": "Nenhum site selecionado" + }, + "summary": { + "title": "Resumo", + "day": "Total de dias ativos", + "firstDay": "Primeira visita {value}" + }, + "trend": { + "title": "Tendências", + "startDate": "Data de início", + "endDate": "Data de fim", + "lastWeek": "Última semana", + "last15Days": "Últimos 15 dias", + "last30Days": "Últimos 30 dias", + "last90Days": "Últimos 90 dias", + "activeDay": "Dias ativos", + "totalDay": "Dias de período", + "maxFocus": "Tempo máximo de navegação diário", + "averageFocus": "Tempo médio de navegação diário", + "maxVisit": "Máximo diário de visitas", + "averageVisit": "Média diária de visitas", + "focusTitle": "Tendências do Tempo de Navegação", + "visitTitle": "Tendências de Visita" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/confirm-resource.json b/src/i18n/message/app/confirm-resource.json index 14d6af83b..87477087c 100644 --- a/src/i18n/message/app/confirm-resource.json +++ b/src/i18n/message/app/confirm-resource.json @@ -14,5 +14,9 @@ "ja": { "confirmMsg": "OK", "cancelMsg": "キャンセル" + }, + "pt": { + "confirmMsg": "OK", + "cancelMsg": "Não!" } } \ No newline at end of file diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json index a37950177..034d203e2 100644 --- a/src/i18n/message/app/dashboard-resource.json +++ b/src/i18n/message/app/dashboard-resource.json @@ -102,5 +102,31 @@ "increase": "増加", "decline": "減らす" } + }, + "pt": { + "heatMap": { + "title0": "Navegou mais de {hour} horas no último ano", + "title1": "Navegue por menos de 1 hora no ano passado", + "tooltip0": "Navegado por {minute} minutos em {day}/{month}/{year}", + "tooltip1": "Navegado por {hour} horas e {minute} minutos em {day}/{month}/{year}" + }, + "topK": { + "title": "TOP {k} mais visitados nos últimos {day} dias", + "tooltip": "Visitou {host} por {visit} vezes" + }, + "indicator": { + "installedDays": "Instalado por {number} dias", + "visitCount": "Visite {site} sites {visit} vezes", + "browsingTime": "Navegado por {minute} minutos", + "mostUse": "Navegação favorita entre {start} e {end} horas" + }, + "weekOnWeek": { + "title": "TOP {k} semana após semana mudança no tempo de navegação", + "lastBrowse": "Navegou {time} na última semana", + "thisBrowse": "Navegou {time} nesta semana", + "wow": "{delta} {state}", + "increase": "aumentou", + "decline": "diminuiu" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/data-manage-resource.json b/src/i18n/message/app/data-manage-resource.json index eb3ca7cc9..28b90d7c0 100644 --- a/src/i18n/message/app/data-manage-resource.json +++ b/src/i18n/message/app/data-manage-resource.json @@ -94,5 +94,29 @@ "migrated": "正常にインポートされました", "operationCancel": "取消", "operationConfirm": "確認" + }, + "pt": { + "totalMemoryAlert": "O navegador fornece {size}MB para armazenar dados locais para cada extensão", + "totalMemoryAlert1": "Não é possível determinar a memória máxima disponível permitida pelo navegador", + "usedMemoryAlert": "{size}MB está atualmente em uso", + "operationAlert": "Você pode excluir esses dados sem importância para reduzir o uso de memória", + "filterItems": "Filtrar dados", + "filterFocus": "O tempo de navegação do dia está entre {start} segundos e {end} segundos", + "filterTime": "O número de visitas do dia está entre {start} e {end}", + "filterDate": "Gravado entre {picker}", + "unlimited": "∞", + "paramError": "O parâmetro está errado, por favor verifique!", + "deleteConfirm": "Um total de {count} registros foram filtrados. Deseja excluir todos eles?", + "deleteSuccess": "Eliminado com êxito!", + "migrationAlert": "Migrar dados entre navegadores usando a importação e exportação", + "importError": "Formato de arquivo incorreto", + "migrated": "Importado com sucesso!", + "operationCancel": "Cancelar", + "operationConfirm": "Confirmar", + "dateShortcut": { + "tillYesterday": "Até ontem", + "till7DaysAgo": "Até 7 dias atrás", + "till30DaysAgo": "Até 30 dias atrás" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json index cc4d0995a..bc7961c91 100644 --- a/src/i18n/message/app/habit-resource.json +++ b/src/i18n/message/app/habit-resource.json @@ -98,5 +98,30 @@ "yAxisMin": "閲覧時間/分", "yAxisHour": "閲覧時間/時間" } + }, + "pt": { + "sizes": { + "fifteen": "Por 15 minutos", + "halfHour": "Por meia hora", + "hour": "Por 1 hora", + "twoHour": "Por 2 horas" + }, + "average": { + "label": "Média diária" + }, + "dateRange": { + "lastDay": "Último dia", + "last3Days": "Últimos 3 dias", + "lastWeek": "Última semana", + "last15Days": "Últimos 15 dias", + "last30Days": "Últimos 30 dias", + "last60Days": "Últimos 60 dias" + }, + "chart": { + "title": "Estatísticas do tempo de navegação", + "saveAsImageTitle": "Capturas", + "yAxisMin": "Tempo de Navegação / Minutos", + "yAxisHour": "Tempo de Navegação / Hora" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index e95a1d2e2..8b2a267d7 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -131,7 +131,28 @@ const _default: Messages = { timeFormat: timeFormatMessages.ja, duration: popupDurationMessages.ja, helpUs: helpUsMessages.ja, - } + }, + pt: { + dataManage: dataManageMessages.pt, + item: itemMessages.pt, + mergeCommon: mergeCommonMessages.pt, + report: reportMessages.pt, + whitelist: whitelistMessages.pt, + mergeRule: mergeRuleMessages.pt, + option: optionMessages.pt, + analysis: analysisMessages.pt, + menu: menuMessages.pt, + habit: habitMessages.pt, + limit: limitMessages.pt, + siteManage: siteManageManages.pt, + operation: operationMessages.pt, + confirm: confirmMessages.pt, + dashboard: dashboardMessages.pt, + calendar: calendarMessages.pt, + timeFormat: timeFormatMessages.pt, + duration: popupDurationMessages.pt, + helpUs: helpUsMessages.pt, + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 2be5daf4c..5299a0e82 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -66,7 +66,7 @@ "noTime": "未填冩每日限製時長", "deleteConfirm": "是否刪除限製:{cond}?", "deleted": "刪除成功", - "noPermissionFirefox": "請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限", + "noPermissionFirefox": "請先在插件管理頁【about:addons】開啟該插件的粘貼闆權限", "inputTestUrl": "請先輸入需要測試的網址鏈接", "clickTestButton": "輸入完成後請點擊【{buttonText}】按鈕", "noRuleMatched": "該網址未命中任何規則", @@ -150,5 +150,43 @@ }, "urlPlaceholder": "URLを直接貼り付けてください➡️", "testUrlLabel": "テスト URL" + }, + "pt": { + "conditionFilter": "URL", + "filterDisabled": "Apenas Habilitadas", + "addTitle": "Criar", + "useWildcard": "Se deseja usar caractere curinga", + "urlPlaceholder": "Cole a URL diretamente ➡️", + "item": { + "condition": "Restrito URL", + "time": "Limite de tempo diário", + "waste": "Tempo de navegação hoje", + "enabled": "Ativado", + "delayAllowed": "Mais 5 minutos", + "delayAllowedInfo": "Se expirar, permita um atraso temporário de 5 minutos", + "operation": "Operações" + }, + "button": { + "add": "Criar", + "test": "Testar URL", + "paste": "Colar", + "save": "Salvar", + "delete": "Apagar", + "testSimple": "Testar", + "modify": "Modificar" + }, + "message": { + "saved": "Guardado com sucesso", + "noUrl": "URL limitada não preenchida", + "noTime": "Tempo por dia não preenchido", + "deleteConfirm": "Deseja excluir a regra de {cond}?", + "deleted": "Apagado com sucesso", + "noPermissionFirefox": "Por favor, ative a permissão da área de transferência para esta extensão na página de gestão (about:addons) primeiro", + "inputTestUrl": "Por favor insira o link de URL para testar primeiro", + "clickTestButton": "Após a entrada, por favor, clique no botão ({buttonText})", + "noRuleMatched": "O URL não atinge nenhuma regra", + "rulesMatched": "A URL atinge as seguintes regras:" + }, + "testUrlLabel": "Testar URL" } } \ No newline at end of file diff --git a/src/i18n/message/app/menu-resource.json b/src/i18n/message/app/menu-resource.json index 84201f88d..71b21f7f1 100644 --- a/src/i18n/message/app/menu-resource.json +++ b/src/i18n/message/app/menu-resource.json @@ -78,5 +78,25 @@ "rate": "それを評価", "helpUs": "協力する", "userManual": "ユーザーマニュアル" + }, + "pt": { + "dashboard": "Painel de Controlo", + "data": "Os Meus Dados", + "dataReport": "Registo", + "dataClear": "Situação da Memória", + "behavior": "Comportamento do Utilizador", + "habit": "Hábitos", + "limit": "Limite diário", + "additional": "Características Adicionais", + "siteManage": "Gestão do Site", + "whitelist": "Lista Ignorada", + "mergeRule": "Regras para Mesclar Site", + "other": "Outras Características", + "option": "Opções", + "feedback": "Questionário de Opinião", + "rate": "Avalie", + "helpUs": "Ajude-nos", + "siteAnalysis": "Análise do Site", + "userManual": "Manual do Utilizador" } } \ No newline at end of file diff --git a/src/i18n/message/app/merge-rule-resource.json b/src/i18n/message/app/merge-rule-resource.json index b1fe8d3ad..e977702a4 100644 --- a/src/i18n/message/app/merge-rule-resource.json +++ b/src/i18n/message/app/merge-rule-resource.json @@ -23,7 +23,7 @@ "addConfirmMsg": "將爲 {origin} 設置自定義合並規則", "infoAlertTitle": "該頁麵可以配置子網域的合並規則", "infoAlert0": "點擊新增按鈕,會彈出原網域和合並後網域的輸入框,填冩並保存規則", - "infoAlert1": "原網域可填具體的網域或者正則表達式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則", + "infoAlert1": "原網域可填具體的網域或者正規表示式,比如 www.baidu.com,*.baidu.com,*.google.com.*。以此確定哪些網域在合並時會使用該條規則", "infoAlert2": "合並後網域可填具體的網域,或者填數字,或者不填", "infoAlert3": "如果填數字,則表示合並後網域的級數。比如存在規則【 *.*.edu.cn >>> 3 】,那麼 www.hust.edu.cn 將被合並至 hust.edu.cn", "infoAlert4": "如果不填,則表示原網域不會被合並", @@ -58,5 +58,20 @@ "infoAlert3": "数字を記入する場合は、ドメイン名のレベルが予約されていることを意味します。 たとえば、ルール [*.*.edu.cn >>> 3 ] がある場合、www.hust.edu.cn は hust.edu.cn にマージされます。", "infoAlert4": "記入しない場合は、元のドメイン名が統合されないことを意味します", "infoAlert5": "一致するルールがない場合、デフォルトで {psl} より前のレベルになります" + }, + "pt": { + "removeConfirmMsg": "{origin} será removido das regras personalizadas de mesclagem.", + "originPlaceholder": "Site original", + "mergedPlaceholder": "Mesclado", + "errorOrigin": "O formato do site original é inválido.", + "duplicateMsg": "A regra já existe: {origin}", + "addConfirmMsg": "Regras de mesclagem personalizadas serão definidas para {origin}", + "infoAlertTitle": "Você pode definir as regras de mesclagem ao contar sites nesta página", + "infoAlert0": "Clique no botão [Criar], as caixas de entrada do site de origem e o site de mesclagem serão exibidas, preencherão e salvarão a regra", + "infoAlert1": "O site original pode ser preenchido com um site específico ou expressão regular, como www.baidu.com, *.baidu.com, *.google.com.*, para determinar quais sites corresponderão a essa regra ao mesclar", + "infoAlert2": "O site mesclado pode ser preenchido com um site específico, um número ou em branco", + "infoAlert3": "Um número significa o nível de um site unificado. Por exemplo, há uma regra '*.*.edu.cn >>> 3', em seguida, 'www.hust.edu.cn' será fundida com 'hust.edu.cn'", + "infoAlert4": "Em branco significa que o site original não será mesclado", + "infoAlert5": "Se nenhuma regra for correspondida, o padrão será o nível antes de {psl}" } } \ No newline at end of file diff --git a/src/i18n/message/app/operation-resource.json b/src/i18n/message/app/operation-resource.json index 0380f970e..767486643 100644 --- a/src/i18n/message/app/operation-resource.json +++ b/src/i18n/message/app/operation-resource.json @@ -22,5 +22,11 @@ "successMsg": "正常に動作しました!", "newOne": "追加", "save": "保存" + }, + "pt": { + "confirmTitle": "Confirmar", + "successMsg": "Com êxito!", + "newOne": "Criar", + "save": "Salvar" } } \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index d451c884e..a944f66ef 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -134,9 +134,9 @@ "title": "統計", "countWhenIdle": "{input} 是否統計 {idleTime} {info}", "idleTime": "休眠時間", - "idleTimeInfo": "長時間不操作(比如全屏觀看視頻),瀏覽器會自動進入休眠狀態", + "idleTimeInfo": "長時間不操作,比如全屏觀看視頻,瀏覽器會自動進入休眠狀態", "countLocalFiles": "{input} 是否統計使用瀏覽器 {localFileTime} {info}", - "localFileTime": "閱讀本地文件的時間", + "localFileTime": " 閱讀本地文件的時間 ", "localFilesInfo": "支持 PDF、圖片、txt 以及 json 等格式", "collectSiteName": "{input} 拜訪網站主頁時,是否自動收集 {siteName} {siteNameUsage}", "siteName": "網站的名稱", @@ -220,10 +220,10 @@ "idleTime": "idle time", "idleTimeInfo": "If you do not operate for a long time (such as watching a video in full screen), the browser will automatically enter the idle state", "countLocalFiles": "{input} Whether to count the time to {localFileTime} {info} in the browser", - "localFileTime": " read a local file ", + "localFileTime": "read a local file", "localFilesInfo": "Supports files of types such as PDF, image, txt and json", "collectSiteName": "{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage", - "siteName": " the site name ", + "siteName": "the site name", "siteNameUsage": "The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site." }, "backup": { @@ -304,9 +304,9 @@ "idleTime": "アイドルタイム", "idleTimeInfo": "長時間操作しない場合(フルスクリーンでビデオを見るなど)、ブラウザは自動的にアイドル状態になります", "countLocalFiles": "{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか", - "localFileTime": " ローカルファイルの読み取り ", + "localFileTime": "ローカルファイルの読み取り", "localFilesInfo": "PDF、画像、txt、jsonを含む", - "collectSiteName": "{input} ウェブサイトのホームページにアクセスしたときにウェブサイトの名前を自動的に収集するかどうか", + "collectSiteName": "{input} サイトのホームページにアクセスしたときに {siteName} {siteNameUsage} を自動的に収集するかどうか", "siteName": "サイト名", "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。" }, @@ -328,11 +328,94 @@ "lastTimeTip": "前回のバックアップ時間: {lastTime}", "auto": { "label": "自動バックアップを有効にするかどうか", - "interval": " {input} 分ごとに実行" + "interval": "{input} 分ごとに実行" } }, "resetButton": "リセット", "resetSuccess": "デフォルトに正常にリセット", "defaultValue": "デフォルト値:{default}" + }, + "pt": { + "yes": "Sim", + "no": "Não", + "resetButton": "Reiniciar", + "resetSuccess": "Redefinido para padrão com sucesso!", + "defaultValue": "Padrão: {default}", + "popup": { + "title": "Página de Pop-up", + "max": "Mostrar os primeiros {input} itens de dados", + "defaultMergeDomain": "{input} Se deseja mesclar subdomínios em aberto", + "defaultDisplay": "Mostrar {type} de {duration} em aberto", + "displaySiteName": "{input} Se deve exibir {siteName} em vez de URL", + "weekStart": "O primeiro dia de cada semana {input}", + "weekStartAsNormal": "Normal" + }, + "appearance": { + "title": "Aparência", + "displayWhitelist": "{input} Se deseja exibir {whitelist} em {contextMenu}", + "whitelistItem": "atalhos relacionados à lista ignorada", + "contextMenu": "o ementa de contexto", + "displayBadgeText": "{input} Se deve exibir {timeInfo} em {icon}", + "icon": "o ícone de extensão", + "badgeTextContent": "o tempo de navegação do site atual", + "locale": { + "label": "Idioma {input}", + "default": "Siga o navegador", + "changeConfirm": "O idioma foi alterado com sucesso, por favor, recarregue esta página!", + "reloadButton": "Recarregar" + }, + "printInConsole": { + "label": "{input} Se deseja imprimir {info} no {console}", + "console": "consola", + "info": "a contagem de visitas do site atual hoje" + }, + "darkMode": { + "label": "Modo escuro {input}", + "options": { + "default": "Siga o navegador", + "on": "Sempre ligado", + "off": "Sempre desativado", + "timed": "Cronometrando" + } + }, + "limitFilterType": { + "label": "Estilo de fundo para limite de tempo diário {input}", + "translucent": "Translúcido", + "groundGlass": "Vidro no Solo" + } + }, + "statistics": { + "title": "Estatísticas", + "countWhenIdle": "{input} Se quer contar {idleTime} {info}", + "idleTime": "tempo ocioso", + "idleTimeInfo": "Se você não operar por um longo período (como ver um vídeo em tela cheia), o navegador entrará automaticamente no estado ocioso", + "countLocalFiles": "{input} Se quer contar o tempo até {localFileTime} {info} no navegador", + "localFileTime": "ler um arquivo local", + "localFilesInfo": "Suporta arquivos de tipos como PDF, imagem, TXT e JSON", + "collectSiteName": "{input} Se deseja coletar automaticamente o {siteName} {siteNameUsage} quando visitar a página inicial do site", + "siteName": "o nome do site", + "siteNameUsage": "Os dados são armazenados apenas localmente e serão exibidos em vez da URL para aumentar o reconhecimento. F, também pode personalizar o nome de cada site." + }, + "backup": { + "title": "Backup de Dados", + "type": "Tipo remoto {input}", + "client": "Nome do cliente {input}", + "meta": { + "none": { + "label": "Sempre desativado" + }, + "gist": { + "authInfo": "É necessário um token com pelo menos gist permissão" + } + }, + "test": "Testar", + "operation": "Backup", + "auto": { + "label": "Se deseja ativar o backup automático", + "interval": "e executar a cada {input} minutos" + }, + "lastTimeTip": "Hora do último backup: {lastTime}", + "alert": "Este é um recurso experimental, se tiver alguma dúvida, entre em contacto com o autor via {email}~" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index 82fd5022e..aa0bf452d 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -126,5 +126,37 @@ "percentage": "パーセンテージ" } } + }, + "pt": { + "startDate": "Data de início", + "endDate": "Data final", + "lastWeek": "Semana passada", + "last30Days": "Últimos 30 dias", + "today": "Hoje", + "yesterday": "Ontem", + "mergeDate": "Mesclar Datas", + "mergeDomain": "Mesclar URL", + "hostPlaceholder": "URL parcial, e depois digite", + "exportFileName": "Meu_Tempo_de_Navegação", + "added2Whitelist": "Adicionada à lista ignorada", + "removeFromWhitelist": "Removido da lista ignorada", + "batchDelete": { + "buttonText": "Apagar em lote", + "noSelectedMsg": "Por favor, selecione a linha que deseja excluir na tabela primeiro", + "confirmMsg": "{count} registros de sites como {example} em {date} serão excluídos!", + "confirmMsgAll": "{count} registros para sites como {example} serão excluídos!", + "confirmMsgRange": "{count} registros para sites como {example} entre {start} e {end} serão excluídos!", + "successMsg": "Lote excluído com sucesso" + }, + "remoteReading": { + "on": "Lendo dados de backup remoto", + "off": "Clique para ler os dados de backup remoto", + "table": { + "client": "Nome do Cliente", + "localData": "Dados Locais", + "value": "Valor", + "percentage": "Percentagem" + } + } } } \ No newline at end of file diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index 41ca0b8c1..c19f2dc7d 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -182,12 +182,60 @@ }, "merged": { "name": "合并", - "info": "複数の関連するドメイン名のマージ統計をカスタマイズできます" + "info": "関連する複数のドメイン名の統計をマージし、マージ ルールをカスタマイズできます。" }, "virtual": { "name": "バーチャル", "info": "Ant Pattern 形式の任意の URL をカウントします。右上隅にカスタムサイトを追加できます" } } + }, + "pt": { + "hostPlaceholder": "URL parcial, e depois digite", + "aliasPlaceholder": "Nome parcial, e depois digite", + "onlyDetected": "Apenas detetado", + "deleteConfirmMsg": "O nome do {host} será apagado", + "column": { + "host": "URL do Site", + "alias": "Nome do Site", + "aliasInfo": "O nome do site será mostrado na página de registro e na página pop-up", + "source": "Nome Fonte", + "type": "Categoria de Sítio Web", + "icon": "Ícone" + }, + "source": { + "user": "usuário mantido", + "detected": "detetado" + }, + "button": { + "add": "Criar", + "delete": "Apagar", + "save": "Salvar" + }, + "form": { + "emptyAlias": "Por favor, insira um nome de site", + "emptyHost": "Por favor, insira um URL de site" + }, + "msg": { + "hostExistWarn": "{host} existe", + "saved": "Guardado", + "existedTag": "EXISTADO", + "mergedTag": "MERGADO", + "virtualTag": "VIRTUAL" + }, + "type": { + "normal": { + "name": "normal", + "info": "estatísticas por nome de domínio" + }, + "merged": { + "name": "mesclado", + "info": "estatísticas de mesclagem de vários domínios relacionados, e as regras de mesclagem podem ser personalizadas" + }, + "virtual": { + "name": "virtual", + "info": "conta qualquer URL no formato Ant Pattern, pode adicionar um site personalizado no canto superior direito" + } + } } } \ No newline at end of file diff --git a/src/i18n/message/app/time-format-resource.json b/src/i18n/message/app/time-format-resource.json index a244f2cba..264df1295 100644 --- a/src/i18n/message/app/time-format-resource.json +++ b/src/i18n/message/app/time-format-resource.json @@ -22,5 +22,11 @@ "hour": "時間単位で表示", "minute": "分単位で表示", "second": "秒単位で表示" + }, + "pt": { + "default": "Formato padrão da hora", + "hour": "Exibir em horas", + "minute": "Exibir em minutos", + "second": "Exibir em segundos" } } \ No newline at end of file diff --git a/src/i18n/message/app/whitelist-resource.json b/src/i18n/message/app/whitelist-resource.json index fe9c424ab..3891ffda5 100644 --- a/src/i18n/message/app/whitelist-resource.json +++ b/src/i18n/message/app/whitelist-resource.json @@ -38,5 +38,15 @@ "infoAlert1": "ホワイトリストのサイトは制限されません。", "placeholder": "URL", "errorInput": "無効なURL" + }, + "pt": { + "addConfirmMsg": "{url} não será mais contado após adicionado à lista ignorada.", + "removeConfirmMsg": "{url} será removido da lista ignorada.", + "duplicateMsg": "Duplicado", + "infoAlertTitle": "Você pode definir uma lista de sites ignorados nesta página", + "infoAlert0": "Sites na lista ignorada não serão contados", + "infoAlert1": "Sites na lista ignorada não serão restritos", + "placeholder": "URL do Site", + "errorInput": "URL inválido do site" } } \ No newline at end of file diff --git a/src/i18n/message/common/base-resource.json b/src/i18n/message/common/base-resource.json index 7f8168a20..251738989 100644 --- a/src/i18n/message/common/base-resource.json +++ b/src/i18n/message/common/base-resource.json @@ -18,5 +18,10 @@ "currentVersion": "バージョン: v{version}", "allFunction": "すべての機能", "guidePage": "ユーザーマニュアル" + }, + "pt": { + "currentVersion": "Versão: {version}", + "allFunction": "Todas as Funções", + "guidePage": "Manual do Utilizador" } } \ No newline at end of file diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index 9c8630ecf..4150b2544 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -19,8 +19,14 @@ }, "ja": { "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", - "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", + "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", "dateFormat": "{y}/{m}/{d}", "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + }, + "pt": { + "weekDays": "Seg|Ter|Qua|Qui|Sex|Sab|Dom", + "months": "Jan|Fev|Mar|Abr|Maio|Jun|Jul|Ago|Set|Out|Nov|Dez", + "dateFormat": "{d}/{m}/{y}", + "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}" } } \ No newline at end of file diff --git a/src/i18n/message/common/content-script-resource.json b/src/i18n/message/common/content-script-resource.json index 6d16de4d2..b941b14c4 100644 --- a/src/i18n/message/common/content-script-resource.json +++ b/src/i18n/message/common/content-script-resource.json @@ -10,7 +10,7 @@ }, "zh_TW": { "consoleLog": "今天您打開了 {time} 次 {host},花費了 {focus} 來瀏覽它。", - "closeAlert": "你可以在【網費很貴】的選項中關閉以上提示!", + "closeAlert": "你可以在【{appName}】的選項中關閉以上提示!", "timeWithHour": "{hour} 小時 {minute} 分 {second} 秒", "timeWithMinute": "{minute} 分 {second} 秒", "timeWithSecond": "{second} 秒", @@ -28,11 +28,20 @@ }, "ja": { "consoleLog": "{host} を {time} 回開いて、今日 {focus} をブラウズしました。", - "closeAlert": "Timer のオプションで上記のヒントをオフにすることができます!", + "closeAlert": "{appName} のオプションで上記のヒントをオフにすることができます!", "timeWithHour": "{hour} 時間 {minute} 分 {second} 秒", "timeWithMinute": "{minute} 分 {second} 秒", "timeWithSecond": "{second} 秒", "timeLimitMsg": "【{appName}】によって制限されています", "more5Minutes": "さらに5分間見てください! ! 約束します!" + }, + "pt": { + "closeAlert": "Você pode desligar as dicas acima na opção do {appName}!", + "timeWithHour": "{hour} horas {minute} minutos {second} segundos", + "timeWithMinute": "{minute} minuto(s) {second} segundo(s)", + "timeWithSecond": "{second} segundo(s)", + "timeLimitMsg": "Foi restrito por [{appName}]", + "more5Minutes": "Mais 5 minutos, por favor!!", + "consoleLog": "Abriu {host} por {time} vez(es) e navegou por {focus} hoje." } } \ No newline at end of file diff --git a/src/i18n/message/common/context-menus-resource.json b/src/i18n/message/common/context-menus-resource.json index eec1a376d..0984c414f 100644 --- a/src/i18n/message/common/context-menus-resource.json +++ b/src/i18n/message/common/context-menus-resource.json @@ -7,8 +7,8 @@ "feedbackPage": "吐槽一下" }, "zh_TW": { - "add2Whitelist": "將{host}加入白名單", - "removeFromWhitelist": "將{host}從白名單移出", + "add2Whitelist": "將 {host} 加入白名單", + "removeFromWhitelist": "將 {host} 從白名單移出", "optionPage": "擴充選項", "repoPage": "源碼下載", "feedbackPage": "吐槽一下" @@ -26,5 +26,12 @@ "optionPage": "拡張設定", "repoPage": "ソースコード", "feedbackPage": "フィードバックの欠如" + }, + "pt": { + "add2Whitelist": "Ignorar {host}", + "removeFromWhitelist": "Habilitar {host}", + "optionPage": "Opções", + "repoPage": "Código Fonte", + "feedbackPage": "Problemas" } } \ No newline at end of file diff --git a/src/i18n/message/common/initial-resource.json b/src/i18n/message/common/initial-resource.json index 6bd31ab64..7c7d56e46 100644 --- a/src/i18n/message/common/initial-resource.json +++ b/src/i18n/message/common/initial-resource.json @@ -30,5 +30,13 @@ "pic": "写真", "txt": "TXT" } + }, + "pt": { + "localFile": { + "json": "Arquivos JSON", + "pdf": "Arquivos PDF", + "pic": "Imagens", + "txt": "Arquivos de Texto" + } } } \ No newline at end of file diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index e9dc8f08a..af2fffaa5 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -70,5 +70,23 @@ "exportWholeData": "インポート", "importWholeData": "書き出す" } + }, + "pt": { + "date": "Data", + "host": "URL do Site", + "focus": "Tempo de Navegação", + "time": "Visitas do Site", + "operation": { + "label": "Operações", + "delete": "Apagar", + "add2Whitelist": "Ignorar", + "removeFromWhitelist": "Habilitar", + "deleteConfirmMsgAll": "Todos os registros de {url} serão apagados!", + "deleteConfirmMsgRange": "Todos os registros de {url} entre {start} e {end} serão apagados!", + "deleteConfirmMsg": "O registro de {url} em {date} será excluído!", + "exportWholeData": "Exportar Dados", + "importWholeData": "Importar Dados", + "analysis": "Análise" + } } } \ No newline at end of file diff --git a/src/i18n/message/common/locale-resource.json b/src/i18n/message/common/locale-resource.json index 3c7d3cfa4..4d3468e36 100644 --- a/src/i18n/message/common/locale-resource.json +++ b/src/i18n/message/common/locale-resource.json @@ -15,14 +15,12 @@ "name": "日本語", "comma": "、" }, - "pl": { - "name": "Polski" - }, "pt": { - "name": "Português" + "name": "Português", + "comma": "," }, - "pt_BR": { - "name": "Portugues, Brasil" + "pl": { + "name": "Polski" }, "ko": { "name": "한국인" diff --git a/src/i18n/message/common/merge-resource.json b/src/i18n/message/common/merge-resource.json index 268a1371c..809d51829 100644 --- a/src/i18n/message/common/merge-resource.json +++ b/src/i18n/message/common/merge-resource.json @@ -22,5 +22,11 @@ "blank": "不合并", "level": "{level} 次ドメイン" } + }, + "pt": { + "tagResult": { + "blank": "Não Mesclar", + "level": "Mantenha o Nível {level}" + } } } \ No newline at end of file diff --git a/src/i18n/message/common/meta-resource.json b/src/i18n/message/common/meta-resource.json index 7ddb027bd..6c783b5f5 100644 --- a/src/i18n/message/common/meta-resource.json +++ b/src/i18n/message/common/meta-resource.json @@ -20,5 +20,10 @@ "marketName": "Time Tracker", "description": "To be the BEST webtime tracker.", "slogan": "Insight & Improve" + }, + "pt": { + "name": "Rastreador de Tempo", + "marketName": "Rastreador de Tempo", + "description": "Ser o MELHOR rastreador de tempo da web." } } \ No newline at end of file diff --git a/src/i18n/message/common/popup-duration-resource.json b/src/i18n/message/common/popup-duration-resource.json index 0ede39dfe..9b3b912b0 100644 --- a/src/i18n/message/common/popup-duration-resource.json +++ b/src/i18n/message/common/popup-duration-resource.json @@ -22,5 +22,11 @@ "thisWeek": "今週の", "thisMonth": "今月の", "last30Days": "過去 30 日間" + }, + "pt": { + "today": "Hoje", + "thisWeek": "Esta Semana", + "thisMonth": "Este Mês", + "last30Days": "Últimos 30 dias" } } \ No newline at end of file diff --git a/src/i18n/message/guide/app-resource.json b/src/i18n/message/guide/app-resource.json index 15f50f913..99920ae18 100644 --- a/src/i18n/message/guide/app-resource.json +++ b/src/i18n/message/guide/app-resource.json @@ -26,5 +26,12 @@ "l1": "拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。", "l2": "アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。", "p2": "ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。" + }, + "pt": { + "title": "Digite a página de gestão", + "p1": "Com base nos ícones, a extensão fornece uma maneira mais conveniente de ver dados. Mas se quiser experimentar todas as funcionalidades, precisa visitar a página de gestão da extensão, por uma das duas seguintes maneiras.", + "l1": "Pode clicar com o botão direito do rato no ícone da extensão e clicar em [{button}] no ementa pop-up.", + "l2": "Também pode encontrar a ligação [{button}] na parte inferior da página pop-up de ícone, basta clicar nele.", + "p2": "A página pop-up e a página de gestão são os principais métodos de interação desta extensão. Após saber como abri-los, pode usá-lo completamente." } } \ No newline at end of file diff --git a/src/i18n/message/guide/backup-resource.json b/src/i18n/message/guide/backup-resource.json index 1a38823c9..fe4e598aa 100644 --- a/src/i18n/message/guide/backup-resource.json +++ b/src/i18n/message/guide/backup-resource.json @@ -51,7 +51,7 @@ "title": "如何查詢其他瀏覽器備份的數據?", "p1": "如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。", "enter": "首先,進入管理頁{link},點擊菜單項【{menuItem}】。", - "enable": "如果 token 設置正確,頁面右上角會出現一個{icon}圖標,點擊它即可開啟遠端查詢。", + "enable": "如果 token 設置正確,頁面右上角會出現一個 {icon} 圖標,點擊它即可開啟遠端查詢。", "wait": "等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。", "tip": "因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。" } @@ -62,17 +62,36 @@ "upload": { "title": "セットアップを完了するための 4 つの簡単なステップ", "prepareToken": "まず、GitHub{link} で Gist 権限を持つトークンを生成します。", - "enter": "オプションページ{link}に入ります。", + "enter": "オプションページ {link} に入ります。", "form": "次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。", "backup": "[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。" }, "query": { "title": "他のブラウザでバックアップされたデータを照会する方法は?", "p1": "上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。", - "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "enter": "まず、管理ページ {link} に入り、メニュー項目 {menuItem} をクリックします。", "enable": "トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。", "wait": "データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。", "tip": "リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。" } + }, + "pt": { + "title": "Backup dos dados com GitHub Gist", + "p1": "Esta extensão permite aos utilizadores fazer backup de dados usando o GitHub Gist{link} através de configurações simples.", + "upload": { + "title": "Quatro passos simples para concluir a instalação", + "prepareToken": "Primeiro, gere um token com permissões gist no GitHub{link}.", + "enter": "Digite a página de opções{link}.", + "form": "Em seguida, selecione GitHub Gist como o método de sincronização e preencha seu token na caixa de entrada que aparece abaixo.", + "backup": "Clique no botão Backup para carregar os dados locais para o seu GitHub Gist." + }, + "query": { + "title": "Como realizar o backup de dados feito por outros navegadores?", + "p1": "Se você definir corretamente o token nos passos acima, poderá consultar dados remotos em apenas três etapas simples.", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "enable": "Se o token estiver definido corretamente, um ícone, como {icon}, aparecerá no canto superior direito da página, clique nele para ativar a consulta remota.", + "wait": "Aguarde a conclusão da consulta de dados e mova o rato sobre o valor para ver os dados de cada cliente.", + "tip": "Como os dados remotos são armazenados em fragmentos mensais, o período de consulta não deve ser muito longo." + } } } \ No newline at end of file diff --git a/src/i18n/message/guide/home-resource.json b/src/i18n/message/guide/home-resource.json index 1d7be3e20..6789c2069 100644 --- a/src/i18n/message/guide/home-resource.json +++ b/src/i18n/message/guide/home-resource.json @@ -18,5 +18,10 @@ "desc": "{appName}は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ちます。これにより、ウェブ習慣を洞察して改善することができます。", "button": "すぐに始めましょう", "download": "{browser} にインストール" + }, + "pt": { + "desc": "O {appName} pode ajudá-lo a controlar o tempo gasto em sites de navegação e a contagem de visitas, com o que pode perceber e melhorar os seus hábitos na web.", + "button": "Comece agora!", + "download": "Instalar para {browser}" } } \ No newline at end of file diff --git a/src/i18n/message/guide/index.ts b/src/i18n/message/guide/index.ts index d11cb7325..7c98e5f3f 100644 --- a/src/i18n/message/guide/index.ts +++ b/src/i18n/message/guide/index.ts @@ -96,6 +96,21 @@ const _default: Messages = { backup: backupMessages.ja, appMenu: appMenuMessages.ja, }, + pt: { + mergeCommon: mergeCommonMessages.pt, + layout: layoutMessages.pt, + home: homeMessages.pt, + start: startMessages.pt, + privacy: privacyMessages.pt, + meta: metaMessages.pt, + base: baseMessages.pt, + app: appMessages.pt, + merge: mergeMessages.pt, + virtual: virtualMessages.pt, + limit: limitMessages.pt, + backup: backupMessages.pt, + appMenu: appMenuMessages.pt, + }, } export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/layout-resource.json b/src/i18n/message/guide/layout-resource.json index 0d6c11e9a..341c0dc3b 100644 --- a/src/i18n/message/guide/layout-resource.json +++ b/src/i18n/message/guide/layout-resource.json @@ -34,5 +34,14 @@ "menu": { "usage": "高度な使い方" } + }, + "pt": { + "header": { + "sourceCode": "Exibir código-fonte", + "email": "Autor do contacto" + }, + "menu": { + "usage": "Uso avançado" + } } } \ No newline at end of file diff --git a/src/i18n/message/guide/limit-resource.json b/src/i18n/message/guide/limit-resource.json index 7ca63f445..cc9a39f41 100644 --- a/src/i18n/message/guide/limit-resource.json +++ b/src/i18n/message/guide/limit-resource.json @@ -37,10 +37,21 @@ "p1": "特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。", "step": { "title": "制限ルールを作成するための 4 つのステップ", - "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "enter": "まず、管理ページ {link} に入り、メニュー項目{menuItem} をクリックします。", "click": "右上隅にある [新規] ボタンをクリックします。", "form": "制限する必要がある URL と制限の期間を貼り付けます。一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。", "check": "最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。" } + }, + "pt": { + "title": "Limitar tempo de navegação de todos os dias", + "p1": "Se deseja limitar o tempo de navegação de certas URL por dia, pode fazê-lo criando uma regra diária de limite de tempo.", + "step": { + "title": "Quatro passos para criar uma regra limite", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "click": "Clique no botão \"Criar\" no canto superior direito.", + "form": "Cole a URL que precisa ser restrita, e a duração da restrição. Alguns fragmentos de URL podem ser substituídos por carácter curinga, conforme necessário, ou excluídos diretamente. Depois clique em Salvar.", + "check": "Finalmente, verifique se a URL de destino atinge a regra recém-adicionada através do botão Testar no canto superior direito." + } } } \ No newline at end of file diff --git a/src/i18n/message/guide/merge-resource.json b/src/i18n/message/guide/merge-resource.json index 92b9f995c..4fd4ebaa7 100644 --- a/src/i18n/message/guide/merge-resource.json +++ b/src/i18n/message/guide/merge-resource.json @@ -98,7 +98,7 @@ "ja": { "title": "関連サイトのデータをまとめます", "p1": "この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。", - "p2": "ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ{link}でマージ ルールをカスタマイズできます。", + "p2": "ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ {link} でマージ ルールをカスタマイズできます。", "lookTitle": "ルールはどのように見えますか?", "p3": "ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。", "sourceCol": "ソース部分", @@ -126,5 +126,37 @@ "mergedCol": "統合サイト", "hitCol": "ヒットルール" } + }, + "pt": { + "title": "Resumir dados de sites relacionados", + "p1": "Esta extensão é contada pelo nome de domínio, por exemplo, {demo1} e {demo2} serão contados como 2 registos. Se quer ver os dados agregados para ambos os sites, precisará usar o recurso de mesclagem.", + "p2": "Na maioria das páginas de exibição de dados, são suportadas consultas mescladas. E os utilizadores podem personalizar as regras de mesclagem na página de segundo plano{link}.", + "lookTitle": "Quais são as regras?", + "p3": "A regra consiste em duas partes, a parte de origem e a parte de destino. A parte de origem declara quais sites atingem a regra, e a parte de destino define como esses sites são mesclados. Por exemplo, {demo} é uma regra comum, a esquerda é o site de origem, e a direita é o site alvo.", + "sourceCol": "Parte fonte", + "targetCol": "Parte alvo", + "remarkCol": "Observação", + "source": { + "title": "Como definir a parte de origem?", + "p1": "A parte de origem pode ser um nome de domínio específico ou uma expressão Ant. Abaixo estão alguns exemplos.", + "exampleCol": "Exemplos de sites correspondentes", + "only": "Somente {source} pode atingir essa regra" + }, + "target": { + "title": "Como definir a parte alvo?", + "p1": "A parte de destino pode ser um domínio específico, um inteiro positivo ou ficar em branco. Elas serão introduzidas uma a uma, em conjunto com a parte de origem na tabela abaixo.", + "lookCol": "Aparência", + "remark": { + "blank": "{source} não será mesclada causa de destino em branco", + "spec": "Sítios web que acertarem {source} serão mesclados no {target} específico", + "integer": "Sítios web que atingirem {source} serão mesclados aos últimos {target} nomes de domínio", + "specFirst": "Quando múltiplas regras são atingidas, a parte de origem tem prioridade para um nome de domínio específico", + "miss": "Mesclar ao nível anterior da lista de sufixos públicos{link} quando nenhuma regra for atingida" + }, + "p2": "A tabela a seguir é alguns exemplos de mesclagem depois que as regras acima são definidas em simultâneo.", + "originalCol": "Site original", + "mergedCol": "Site mesclado", + "hitCol": "Regra negada" + } } } \ No newline at end of file diff --git a/src/i18n/message/guide/privacy-resource.json b/src/i18n/message/guide/privacy-resource.json index 2347b6494..306762d15 100644 --- a/src/i18n/message/guide/privacy-resource.json +++ b/src/i18n/message/guide/privacy-resource.json @@ -134,5 +134,39 @@ "p2": "ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。", "p3": "私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません." } + }, + "pt": { + "title": "Declaração de privacidade", + "alert": "Para fornecer serviços completos, esta extensão irá necessariamente coletar alguns dos seus dados pessoais durante o uso, consulte a seguinte declaração de privacidade para obter detalhes.", + "scope": { + "title": "Quais as informações recolhidas?", + "cols": { + "name": "Conteúdo", + "usage": "Utilização", + "required": "Requeridas" + }, + "rows": { + "website": { + "name": "Histórico de navegação de sites", + "usage": "Usado para contar o tempo de navegação e visitas" + }, + "tab": { + "name": "Informação de guia", + "usage": "Usado para obter automaticamente o nome e o ícone do site, e melhorar a experiência do utilizador ao exibir dados", + "optionalReason": "Somente se esta função estiver ativada nas opções" + }, + "clipboard": { + "name": "Conteúdo da área de transferência", + "usage": "Ao definir a regra de limite de tempo diário, para a conveniência da operação, a URL da área de transferência será lida", + "optionalReason": "Somente se utilizador concordar" + } + } + }, + "storage": { + "title": "Como fazer com esses dados?", + "p1": "Garantimos que todos os dados coletados por esta extensão só serão salvos no armazenamento local do seu navegador e nunca serão distribuídos em outro lugar.", + "p2": "No entanto, pode usar as ferramentas fornecidas pela extensão para exportar ou importar os seus dados em formato JSON, ou CSV. A extensão também oferece suporte a você para usar o GitHub Gist, etc., serviços de terceiros nos quais confia o suficiente para fazer backup dos seus dados.", + "p3": "Só lhe ajudamos a recolher dados, mas o direito à eliminação deve ser o seu." + } } } \ No newline at end of file diff --git a/src/i18n/message/guide/start-resource.json b/src/i18n/message/guide/start-resource.json index aa4f0de37..946c1535d 100644 --- a/src/i18n/message/guide/start-resource.json +++ b/src/i18n/message/guide/start-resource.json @@ -42,5 +42,16 @@ "s3": "3. ポップアップ ページでデータを読み取る", "s3p1": "最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。", "alert": "基本的な使い方を学んだので、試してみてください!!" + }, + "pt": { + "title": "Iniciar", + "p1": "Pode começar rapidamente a usar essa extensão em apenas 3 etapas fáceis.", + "s1": "1. Fixar o ícone", + "s1p1": "Em primeiro lugar, para usar esta extensão mais convenientemente, é melhor fixar o ícone na barra de ferramentas. Não é o mesmo em diferentes navegadores fazer isso, a seguinte figura mostra como no Chrome.", + "s2": "2. Procure qualquer site", + "s2p1": "Em seguida, navegue em qualquer site, e verá que o tempo está batendo no ícone, assim como este {demo}.", + "s3": "3. Ler dados na página pop-up", + "s3p1": "Finalmente, clique no ícone para abrir a página do pop-up, e pode ler os dados visualizados com gráfico de pizza, de hoje, esta semana ou este mês.", + "alert": "Aprendeu o uso básico, experimente!!" } } \ No newline at end of file diff --git a/src/i18n/message/guide/virtual-resource.json b/src/i18n/message/guide/virtual-resource.json index 4b02f4e7b..7cb72c62d 100644 --- a/src/i18n/message/guide/virtual-resource.json +++ b/src/i18n/message/guide/virtual-resource.json @@ -37,10 +37,21 @@ "p1": "ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。", "step": { "title": "仮想サイトを作成するための 4 つのステップ", - "enter": "まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。", + "enter": "まず、管理ページ {link} に入り、メニュー項目 {menuItem} をクリックします。", "click": "右上隅にある [新規] ボタンをクリックします。", "form": "次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。", "browse": "最後に、関連する URL を参照してデータを観察します。" } + }, + "pt": { + "title": "Contar dados para URL específicos", + "p1": "Se deseja contar o tempo de navegação e o número de visitas de certas URL, não apenas o nome de domínio, pode fazê-lo criando um site virtual.", + "step": { + "title": "Quatro etapas para criar um site virtual", + "enter": "Primeiro, digite a página de gestão{link}, clique no item de menu {menuItem}.", + "click": "Clique no botão Criar no canto superior direito.", + "form": "Em seguida, preencha a URL para contar, expressões Ant são permitidas, como {demo1} ou {demo2}, o nome deste site virtual e clique em Salvar.", + "browse": "Finalmente, procure os URL relevantes e observe os dados." + } } } \ No newline at end of file diff --git a/src/i18n/message/popup/chart-resource.json b/src/i18n/message/popup/chart-resource.json index b8a68d6f8..7cd1b3f5b 100644 --- a/src/i18n/message/popup/chart-resource.json +++ b/src/i18n/message/popup/chart-resource.json @@ -36,7 +36,7 @@ "totalCount": "共 {totalCount} 次", "averageCount": "平均每天 {value} 次", "averageTime": "平均每天 {value}", - "otherLabel": "其他{count}個站點", + "otherLabel": "其他 {count} 個站點", "updateVersion": "版本昇級", "updateVersionInfo": "最新版本:{version}", "updateVersionInfo4Firefox": "新版本 {version} 已髮佈\n\n您可以前往插件管理頁進行更新" @@ -78,9 +78,30 @@ "totalCount": "合計 {totalCount} 回", "averageTime": "1日平均 {value}", "averageCount": "1日平均 {value} 回", - "otherLabel": "他{count}サイト", + "otherLabel": "他 {count} サイト", "updateVersion": "更新", "updateVersionInfo": "最新バージョン:{version}", "updateVersionInfo4Firefox": "管理ページで {version} にアップグレードしてください" + }, + "pt": { + "mergeHostLabel": "Mesclar Sítios Web", + "fileName": "Lista_de_Horários_da_Web_{today}_por_{app}", + "saveAsImageTitle": "Capturas", + "restoreTitle": "Restaurar", + "options": "Opções", + "totalTime": "Total {totalTime}", + "totalCount": "Total {totalCount} vezes", + "averageCount": "{value} vezes por dia em média", + "averageTime": "{value} por dia em média", + "otherLabel": "Outros sites do {count}", + "updateVersion": "Atualizável", + "updateVersionInfo": "Último: {version}", + "updateVersionInfo4Firefox": "Atualize para {version} na página de gestão, about:addons, por favor", + "title": { + "today": "Dados de Hoje", + "thisWeek": "Dados desta Semana", + "thisMonth": "Dados deste Mês", + "last30Days": "Dados dos últimos 30 dias" + } } } \ No newline at end of file diff --git a/src/i18n/message/popup/index.ts b/src/i18n/message/popup/index.ts index 47cefa183..b2d94b05f 100644 --- a/src/i18n/message/popup/index.ts +++ b/src/i18n/message/popup/index.ts @@ -48,6 +48,13 @@ const _default: Messages = { meta: metaMessages.ja, base: baseMessages.ja, }, + pt: { + chart: chartMessages.pt, + duration: popupDurationMessages.pt, + item: itemMessages.pt, + meta: metaMessages.pt, + base: baseMessages.pt, + }, } export default _default \ No newline at end of file diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index bb10575bb..188981a0a 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -66,7 +66,8 @@ export const UNINSTALL_QUESTIONNAIRE: { [locale in timer.Locale]: string } = { zh_CN: 'https://www.wjx.cn/vj/YDgY9Yz.aspx', zh_TW: 'https://docs.google.com/forms/d/e/1FAIpQLSdK93q-548dK-2naoS3DaArdc7tEGoUY9JQvaXP5Kpov8h6-A/viewform?usp=sf_link', ja: 'https://docs.google.com/forms/d/e/1FAIpQLSdsB3onZuleNf6j7KJJLbcote647WV6yeUr-9m7Db5QXakfpg/viewform?usp=sf_link', - en: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link' + en: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', + pt: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', } /** diff --git a/types/timer/index.d.ts b/types/timer/index.d.ts index 1809c54bd..c9974ba21 100644 --- a/types/timer/index.d.ts +++ b/types/timer/index.d.ts @@ -15,6 +15,8 @@ declare namespace timer { | 'ja' // @since 0.9.0 | 'zh_TW' + // @since 1.8.2 + | 'pt' /** * @since 0.8.0 */ @@ -30,8 +32,6 @@ declare namespace timer { | 'es' | 'ko' | 'pl' - | 'pt' - | 'pt_BR' | 'ru' | 'uk' | 'fr' From d609edd8cd15523a6b0c09074b5da6e67fb069cd Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 17 May 2023 21:21:07 +0800 Subject: [PATCH 155/168] Remove duplicated keys (#212) --- .../analysis/components/trend/filter.ts | 8 +- .../clear/filter/operation-button.ts | 4 +- src/app/components/data-manage/clear/index.ts | 4 +- src/app/components/habit/component/filter.ts | 15 +-- src/app/components/limit/filter.ts | 2 +- src/app/components/limit/modify/footer.ts | 2 +- src/app/components/limit/modify/form/url.ts | 2 +- .../limit/table/column/operation.ts | 4 +- src/app/components/limit/test.ts | 99 +++++++++---------- src/app/components/report/filter/index.ts | 15 +-- .../rule-merge/components/add-button.ts | 2 +- .../whitelist/components/add-button.ts | 2 +- src/i18n/message/app/analysis-resource.json | 30 ------ src/i18n/message/app/analysis.ts | 6 -- .../message/app/data-manage-resource.json | 18 +--- src/i18n/message/app/data-manage.ts | 2 - src/i18n/message/app/habit-resource.json | 40 -------- src/i18n/message/app/habit.ts | 8 -- src/i18n/message/app/index.ts | 9 +- src/i18n/message/app/limit-resource.json | 53 ++-------- src/i18n/message/app/limit.ts | 7 -- src/i18n/message/app/operation-resource.json | 24 ++--- src/i18n/message/app/operation.ts | 2 - src/i18n/message/app/report-resource.json | 30 ------ src/i18n/message/app/report.ts | 6 -- src/i18n/message/common/button-resource.json | 62 ++++++++++++ src/i18n/message/common/button.ts | 23 +++++ .../message/common/calendar-resource.json | 84 +++++++++++++++- src/i18n/message/common/calendar.ts | 15 +++ 29 files changed, 286 insertions(+), 292 deletions(-) create mode 100644 src/i18n/message/common/button-resource.json create mode 100644 src/i18n/message/common/button.ts diff --git a/src/app/components/analysis/components/trend/filter.ts b/src/app/components/analysis/components/trend/filter.ts index ceb940422..518ae924c 100644 --- a/src/app/components/analysis/components/trend/filter.ts +++ b/src/app/components/analysis/components/trend/filter.ts @@ -7,22 +7,22 @@ import type { ElementDatePickerShortcut } from "@src/element-ui/date" import type { PropType, Ref } from "vue" +import type { CalendarMessage } from "@i18n/message/common/calendar" import { t } from "@app/locale" -import { AnalysisMessage } from "@i18n/message/app/analysis" import { ElDatePicker } from "element-plus" import { defineComponent, h, ref } from "vue" import { daysAgo } from "@util/time" -function datePickerShortcut(msgKey: keyof AnalysisMessage['trend'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { +function datePickerShortcut(msgKey: keyof CalendarMessage['range'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { return { - text: t(msg => msg.analysis.trend[msgKey]), + text: t(msg => msg.calendar.range[msgKey]), value: daysAgo(agoOfStart - 1 || 0, agoOfEnd || 0) } } const SHORTCUTS = [ - datePickerShortcut('lastWeek', 7), + datePickerShortcut('last7Days', 7), datePickerShortcut('last15Days', 15), datePickerShortcut('last30Days', 30), datePickerShortcut("last90Days", 90) 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 d96b1bf24..aeb20b9f9 100644 --- a/src/app/components/data-manage/clear/filter/operation-button.ts +++ b/src/app/components/data-manage/clear/filter/operation-button.ts @@ -111,8 +111,8 @@ function generateParamAndSelect(props: _Props): Promise | unde return statDatabase.select(condition) } -const operationCancelMsg = t(msg => msg.dataManage.operationCancel) -const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) +const operationCancelMsg = t(msg => msg.button.cancel) +const operationConfirmMsg = t(msg => msg.button.confirm) const handleClick = async (props: _Props) => { const result: timer.stat.Row[] = await generateParamAndSelect(props) diff --git a/src/app/components/data-manage/clear/index.ts b/src/app/components/data-manage/clear/index.ts index 40720fdb1..a06784ded 100644 --- a/src/app/components/data-manage/clear/index.ts +++ b/src/app/components/data-manage/clear/index.ts @@ -19,8 +19,8 @@ type _Emits = { const statDatabase = new StatDatabase(chrome.storage.local) -const operationCancelMsg = t(msg => msg.dataManage.operationCancel) -const operationConfirmMsg = t(msg => msg.dataManage.operationConfirm) +const operationCancelMsg = t(msg => msg.button.cancel) +const operationConfirmMsg = t(msg => msg.button.confirm) async function handleClick(filterRef: Ref, ctx: SetupContext<_Emits>) { const filterOption: DataManageClearFilterOption = filterRef?.value?.getFilterOption() diff --git a/src/app/components/habit/component/filter.ts b/src/app/components/habit/component/filter.ts index 889ab5d5b..83abc13a9 100644 --- a/src/app/components/habit/component/filter.ts +++ b/src/app/components/habit/component/filter.ts @@ -5,10 +5,11 @@ * https://opensource.org/licenses/MIT */ -import type { PropType } from "vue" +import type { Ref, PropType } from "vue" import type { HabitMessage } from "@i18n/message/app/habit" +import type { CalendarMessage } from "@i18n/message/common/calendar" -import { ref, Ref, h, defineComponent } from "vue" +import { ref, h, defineComponent } from "vue" import { daysAgo } from "@util/time" import { t } from "@app/locale" import SwitchFilterItem from "@app/components/common/switch-filter-item" @@ -16,20 +17,20 @@ import { ElementDatePickerShortcut } from "@src/element-ui/date" import DateRangeFilterItem from "@app/components/common/date-range-filter-item" import SelectFilterItem from "@app/components/common/select-filter-item" -type ShortCutProp = [label: keyof HabitMessage['dateRange'], dayAgo: number] +type ShortCutProp = [label: keyof CalendarMessage['range'], dayAgo: number] const shortcutProps: ShortCutProp[] = [ - ["lastDay", 1], + ["last24Hours", 1], ["last3Days", 3], - ["lastWeek", 7], + ["last7Days", 7], ["last15Days", 15], ["last30Days", 30], ["last60Days", 60] ] -function datePickerShortcut(msg: keyof HabitMessage['dateRange'], agoOfStart: number): ElementDatePickerShortcut { +function datePickerShortcut(msg: keyof CalendarMessage['range'], agoOfStart: number): ElementDatePickerShortcut { return { - text: t(messages => messages.habit.dateRange[msg]), + text: t(messages => messages.calendar.range[msg]), value: daysAgo(agoOfStart, 0) } } diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts index dd8ef2385..24602faa0 100644 --- a/src/app/components/limit/filter.ts +++ b/src/app/components/limit/filter.ts @@ -14,7 +14,7 @@ import { t } from "@app/locale" const urlPlaceholder = t(msg => msg.limit.conditionFilter) const onlyEnabledLabel = t(msg => msg.limit.filterDisabled) -const addButtonText = t(msg => msg.limit.button.add) +const addButtonText = t(msg => msg.button.create) const testButtonText = t(msg => msg.limit.button.test) const emits = { diff --git a/src/app/components/limit/modify/footer.ts b/src/app/components/limit/modify/footer.ts index 8e1f3a67e..be9744ee6 100644 --- a/src/app/components/limit/modify/footer.ts +++ b/src/app/components/limit/modify/footer.ts @@ -10,7 +10,7 @@ import { ElButton } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" -const buttonText = t(msg => msg.limit.button.save) +const buttonText = t(msg => msg.button.save) const _default = defineComponent({ name: "SaveButton", emits: { diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index 79b3aabbe..be5c7c0a7 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -82,7 +82,7 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler urlHandler?.(cleanUrl(url)) } -const pasteButtonText = t(msg => msg.limit.button.paste) +const pasteButtonText = t(msg => msg.button.paste) const placeholder = t(msg => msg.limit.urlPlaceholder) const _default = defineComponent({ diff --git a/src/app/components/limit/table/column/operation.ts b/src/app/components/limit/table/column/operation.ts index a42e68094..e951c6bd4 100644 --- a/src/app/components/limit/table/column/operation.ts +++ b/src/app/components/limit/table/column/operation.ts @@ -11,8 +11,8 @@ import { defineComponent, h } from "vue" import { t } from "@app/locale" const label = t(msg => msg.limit.item.operation) -const deleteButtonText = t(msg => msg.limit.button.delete) -const modifyButtonText = t(msg => msg.limit.button.modify) +const deleteButtonText = t(msg => msg.button.delete) +const modifyButtonText = t(msg => msg.button.modify) const _default = defineComponent({ name: "LimitOperationColumn", emits: { diff --git a/src/app/components/limit/test.ts b/src/app/components/limit/test.ts index 5b5f0295e..ced70be6a 100644 --- a/src/app/components/limit/test.ts +++ b/src/app/components/limit/test.ts @@ -20,7 +20,7 @@ function computeResultTitle(url: string, inputting: boolean, matchedCondition: s return t(msg => msg.limit.message.inputTestUrl) } if (inputting) { - return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.limit.button.testSimple) }) + return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.button.test) }) } if (!matchedCondition?.length) { return t(msg => msg.limit.message.noRuleMatched) @@ -45,58 +45,55 @@ function computeResultType(url: string, inputting: boolean, matchedCondition: st return matchedCondition?.length ? 'success' : 'warning' } -const _default = defineComponent({ - name: "LimitTest", - setup(_props, ctx) { - const urlRef: Ref = ref() - const matchedConditionRef: Ref = ref([]) - const visible: Ref = ref(false) - const urlInputtingRef: Ref = ref(true) - const resultTitleRef: ComputedRef = computed(() => computeResultTitle(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const resultTypeRef: ComputedRef<_ResultType> = computed(() => computeResultType(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const resultDescRef: ComputedRef = computed(() => computeResultDesc(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) +const _default = defineComponent((_props, ctx) => { + const urlRef: Ref = ref() + const matchedConditionRef: Ref = ref([]) + const visible: Ref = ref(false) + const urlInputtingRef: Ref = ref(true) + const resultTitleRef: ComputedRef = computed(() => computeResultTitle(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultTypeRef: ComputedRef<_ResultType> = computed(() => computeResultType(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) + const resultDescRef: ComputedRef = computed(() => computeResultDesc(urlRef.value, urlInputtingRef.value, matchedConditionRef.value)) - const changeInput = (newVal: string) => (urlInputtingRef.value = true) && (urlRef.value = newVal?.trim()) - const test = () => { - urlInputtingRef.value = false - handleTest(urlRef.value).then(matched => matchedConditionRef.value = matched) - } - - ctx.expose({ - show() { - urlRef.value = '' - visible.value = true - urlInputtingRef.value = true - matchedConditionRef.value = [] - } - }) - return () => h(ElDialog, { - title: t(msg => msg.limit.button.testSimple), - modelValue: visible.value, - closeOnClickModal: false, - onClose: () => visible.value = false - }, () => [ - h(ElFormItem, { - label: t(msg => msg.limit.testUrlLabel), - labelWidth: 120 - }, () => h(ElInput, { - modelValue: urlRef.value, - clearable: true, - onClear: () => changeInput(''), - onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && test(), - onInput: (newVal: string) => changeInput(newVal) - }, { - append: () => h(ElButton, { - onClick: () => test() - }, () => t(msg => msg.limit.button.testSimple)), - })), - h(ElAlert, { - closable: false, - type: resultTypeRef.value, - title: resultTitleRef.value, - }, () => resultDescRef.value.map(desc => h('li', desc))) - ]) + const changeInput = (newVal: string) => (urlInputtingRef.value = true) && (urlRef.value = newVal?.trim()) + const test = () => { + urlInputtingRef.value = false + handleTest(urlRef.value).then(matched => matchedConditionRef.value = matched) } + + ctx.expose({ + show() { + urlRef.value = '' + visible.value = true + urlInputtingRef.value = true + matchedConditionRef.value = [] + } + }) + return () => h(ElDialog, { + title: t(msg => msg.button.test), + modelValue: visible.value, + closeOnClickModal: false, + onClose: () => visible.value = false + }, () => [ + h(ElFormItem, { + label: t(msg => msg.limit.button.test), + labelWidth: 120 + }, () => h(ElInput, { + modelValue: urlRef.value, + clearable: true, + onClear: () => changeInput(''), + onKeyup: (event: KeyboardEvent) => event.key === 'Enter' && test(), + onInput: (newVal: string) => changeInput(newVal) + }, { + append: () => h(ElButton, { + onClick: () => test() + }, () => t(msg => msg.button.test)), + })), + h(ElAlert, { + closable: false, + type: resultTypeRef.value, + title: resultTitleRef.value, + }, () => resultDescRef.value.map(desc => h('li', desc))) + ]) }) export default _default \ No newline at end of file diff --git a/src/app/components/report/filter/index.ts b/src/app/components/report/filter/index.ts index 865f0f41b..6c973b771 100644 --- a/src/app/components/report/filter/index.ts +++ b/src/app/components/report/filter/index.ts @@ -7,7 +7,7 @@ import type { Ref, PropType } from "vue" import type { ElementDatePickerShortcut } from "@src/element-ui/date" -import type { ReportMessage } from "@i18n/message/app/report" +import type { CalendarMessage } from "@i18n/message/common/calendar" import DownloadFile from "./download-file" import RemoteClient from "./remote-client" @@ -34,11 +34,11 @@ const timeFormatLabels: { [key in timer.app.TimeFormat]: string } = { // Batch Delete const batchDeleteButtonText = t(msg => msg.report.batchDelete.buttonText) // Date range -const dateStartPlaceholder = t(msg => msg.report.startDate) -const dateEndPlaceholder = t(msg => msg.report.endDate) +const dateStartPlaceholder = t(msg => msg.calendar.label.startDate) +const dateEndPlaceholder = t(msg => msg.calendar.label.endDate) // date range -function datePickerShortcut(msg: keyof ReportMessage, agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { - const text = t(messages => messages.report[msg]) +function datePickerShortcut(msg: keyof CalendarMessage['range'], agoOfStart?: number, agoOfEnd?: number): ElementDatePickerShortcut { + const text = t(messages => messages.calendar.range[msg]) const value = daysAgo(agoOfStart || 0, agoOfEnd || 0) return { text, value } } @@ -46,8 +46,9 @@ function datePickerShortcut(msg: keyof ReportMessage, agoOfStart?: number, agoOf const dateShortcuts: ElementDatePickerShortcut[] = [ datePickerShortcut('today'), datePickerShortcut('yesterday', 1, 1), - datePickerShortcut('lastWeek', 7), - datePickerShortcut('last30Days', 30) + datePickerShortcut('last7Days', 7), + datePickerShortcut('last30Days', 30), + datePickerShortcut('last60Days', 60), ] const _default = defineComponent({ diff --git a/src/app/components/rule-merge/components/add-button.ts b/src/app/components/rule-merge/components/add-button.ts index 68cf25f15..d9345e50e 100644 --- a/src/app/components/rule-merge/components/add-button.ts +++ b/src/app/components/rule-merge/components/add-button.ts @@ -11,7 +11,7 @@ import { ElButton } from "element-plus" import { defineComponent, h, ref, Ref } from "vue" import ItemInput from './item-input' -const buttonText = `+ ${t(msg => msg.operation.newOne)}` +const buttonText = `+ ${t(msg => msg.button.create)}` const _default = defineComponent({ name: "MergeRuleAddButton", diff --git a/src/app/components/whitelist/components/add-button.ts b/src/app/components/whitelist/components/add-button.ts index 6966c96c7..bca955295 100644 --- a/src/app/components/whitelist/components/add-button.ts +++ b/src/app/components/whitelist/components/add-button.ts @@ -10,7 +10,7 @@ import { ElButton } from "element-plus" import { defineComponent, h, ref, Ref } from "vue" import ItemInput from './item-input' -const buttonText = `+ ${t(msg => msg.operation.newOne)}` +const buttonText = `+ ${t(msg => msg.button.create)}` const _default = defineComponent({ name: "WhitelistAddButton", diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json index 4d80b3243..0a1edb533 100644 --- a/src/i18n/message/app/analysis-resource.json +++ b/src/i18n/message/app/analysis-resource.json @@ -16,12 +16,6 @@ }, "trend": { "title": "区间趋势", - "startDate": "开始日期", - "endDate": "结束日期", - "lastWeek": "最近 7 天", - "last15Days": "最近 15 天", - "last30Days": "最近 30 天", - "last90Days": "最近 90 天", "activeDay": "活跃天数", "totalDay": "区间总天数", "maxFocus": "单日最大浏览时长", @@ -49,12 +43,6 @@ }, "trend": { "title": "區間趨勢", - "startDate": "開始日期", - "endDate": "結束日期", - "lastWeek": "最近 7 天", - "last15Days": "最近 15 天", - "last30Days": "最近 30 天", - "last90Days": "最近 90 天", "activeDay": "活躍天數", "totalDay": "區間總天數", "maxFocus": "單日最大瀏覽時長", @@ -82,12 +70,6 @@ }, "trend": { "title": "Trends", - "startDate": "Start date", - "endDate": "End date", - "lastWeek": "Last week", - "last15Days": "Last 15 days", - "last30Days": "Last 30 days", - "last90Days": "Last 90 days", "activeDay": "Active days", "totalDay": "Period days", "maxFocus": "Daily maximum browsing time", @@ -115,12 +97,6 @@ }, "trend": { "title": "レンジトレンド", - "startDate": "開始日", - "endDate": "終了日", - "lastWeek": "過去 7 日間", - "last15Days": "過去 15 日間", - "last30Days": "過去 30 日間", - "last90Days": "過去 90 日間", "activeDay": "アクティブな日", "totalDay": "間隔の合計日数", "maxFocus": "1 日の最大閲覧時間", @@ -148,12 +124,6 @@ }, "trend": { "title": "Tendências", - "startDate": "Data de início", - "endDate": "Data de fim", - "lastWeek": "Última semana", - "last15Days": "Últimos 15 dias", - "last30Days": "Últimos 30 dias", - "last90Days": "Últimos 90 dias", "activeDay": "Dias ativos", "totalDay": "Dias de período", "maxFocus": "Tempo máximo de navegação diário", diff --git a/src/i18n/message/app/analysis.ts b/src/i18n/message/app/analysis.ts index 4246ffc1b..d9cdd068f 100644 --- a/src/i18n/message/app/analysis.ts +++ b/src/i18n/message/app/analysis.ts @@ -24,12 +24,6 @@ export type AnalysisMessage = { } trend: { title: string - startDate: string, - endDate: string - lastWeek: string - last15Days: string - last30Days: string - last90Days: string activeDay: string totalDay: string maxFocus: string diff --git a/src/i18n/message/app/data-manage-resource.json b/src/i18n/message/app/data-manage-resource.json index 28b90d7c0..f568cd871 100644 --- a/src/i18n/message/app/data-manage-resource.json +++ b/src/i18n/message/app/data-manage-resource.json @@ -19,9 +19,7 @@ "deleteSuccess": "删除成功", "migrationAlert": "使用导入/导出在不同浏览器之间迁移数据", "importError": "文件格式错误", - "migrated": "成功导入", - "operationCancel": "取消", - "operationConfirm": "确认" + "migrated": "成功导入" }, "zh_TW": { "totalMemoryAlert": "瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據", @@ -43,9 +41,7 @@ "deleteSuccess": "刪除成功", "migrationAlert": "使用導入/導出在不同瀏覽器之間遷移數據", "importError": "文件格式錯誤", - "migrated": "成功導入", - "operationCancel": "取消", - "operationConfirm": "確認" + "migrated": "成功導入" }, "en": { "totalMemoryAlert": "The browser provides {size}MB to store local data for each extension", @@ -67,9 +63,7 @@ "deleteSuccess": "Deleted successfully!", "migrationAlert": "Migrate data between browsers using import and export", "importError": "Wrong file extension", - "migrated": "Imported successfully!", - "operationCancel": "Cancel", - "operationConfirm": "Confirm" + "migrated": "Imported successfully!" }, "ja": { "totalMemoryAlert": "ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します", @@ -91,9 +85,7 @@ "deleteSuccess": "正常に削除されました", "migrationAlert": "インポート/エクスポートを使用して、異なるブラウザ間でデータを移行します", "importError": "ファイル形式エラー", - "migrated": "正常にインポートされました", - "operationCancel": "取消", - "operationConfirm": "確認" + "migrated": "正常にインポートされました" }, "pt": { "totalMemoryAlert": "O navegador fornece {size}MB para armazenar dados locais para cada extensão", @@ -111,8 +103,6 @@ "migrationAlert": "Migrar dados entre navegadores usando a importação e exportação", "importError": "Formato de arquivo incorreto", "migrated": "Importado com sucesso!", - "operationCancel": "Cancelar", - "operationConfirm": "Confirmar", "dateShortcut": { "tillYesterday": "Até ontem", "till7DaysAgo": "Até 7 dias atrás", diff --git a/src/i18n/message/app/data-manage.ts b/src/i18n/message/app/data-manage.ts index 1cac603f6..5c429d15b 100644 --- a/src/i18n/message/app/data-manage.ts +++ b/src/i18n/message/app/data-manage.ts @@ -23,8 +23,6 @@ export type DataManageMessage = { migrationAlert: string importError: string migrated: string - operationCancel: string - operationConfirm: string dateShortcut: { tillYesterday: string till7DaysAgo: string diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json index bc7961c91..8a8170063 100644 --- a/src/i18n/message/app/habit-resource.json +++ b/src/i18n/message/app/habit-resource.json @@ -9,14 +9,6 @@ "average": { "label": "平均每天" }, - "dateRange": { - "lastDay": "最近 24 小时", - "last3Days": "最近 3 天", - "lastWeek": "最近 7 天", - "last15Days": "最近 15 天", - "last30Days": "最近 30 天", - "last60Days": "最近 60 天" - }, "chart": { "title": "上网习惯统计", "saveAsImageTitle": "保存", @@ -34,14 +26,6 @@ "average": { "label": "平均每天" }, - "dateRange": { - "lastDay": "最近 24 小時", - "last3Days": "最近 3 天", - "lastWeek": "最近 7 天", - "last15Days": "最近 15 天", - "last30Days": "最近 30 天", - "last60Days": "最近 60 天" - }, "chart": { "title": "上網習慣統計", "saveAsImageTitle": "保存", @@ -59,14 +43,6 @@ "average": { "label": "Daily average" }, - "dateRange": { - "lastDay": "Last day", - "last3Days": "Last 3 days", - "lastWeek": "Last week", - "last15Days": "Last 15 days", - "last30Days": "Last 30 days", - "last60Days": "Last 60 days" - }, "chart": { "title": "Time-phased Statistics of Browsing Time", "saveAsImageTitle": "Snapshot", @@ -84,14 +60,6 @@ "average": { "label": "1日平均" }, - "dateRange": { - "lastDay": "過去24時間", - "last3Days": "過去3日間", - "lastWeek": "先週", - "last15Days": "過去15日間", - "last30Days": "過去30日間", - "last60Days": "過去60日間" - }, "chart": { "title": "時系列の統計を閲覧する", "saveAsImageTitle": "ダウンロード", @@ -109,14 +77,6 @@ "average": { "label": "Média diária" }, - "dateRange": { - "lastDay": "Último dia", - "last3Days": "Últimos 3 dias", - "lastWeek": "Última semana", - "last15Days": "Últimos 15 dias", - "last30Days": "Últimos 30 dias", - "last60Days": "Últimos 60 dias" - }, "chart": { "title": "Estatísticas do tempo de navegação", "saveAsImageTitle": "Capturas", diff --git a/src/i18n/message/app/habit.ts b/src/i18n/message/app/habit.ts index 3e417ce87..5f3958a9a 100644 --- a/src/i18n/message/app/habit.ts +++ b/src/i18n/message/app/habit.ts @@ -17,14 +17,6 @@ export type HabitMessage = { average: { label: string }, - dateRange: { - lastDay: string - last3Days: string - lastWeek: string - last15Days: string - last30Days: string - last60Days: string - }, chart: { title: string saveAsImageTitle: string diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 8b2a267d7..26be11bea 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -21,9 +21,10 @@ import operationMessages, { OperationMessage } from './operation' import confirmMessages, { ConfirmMessage } from './confirm' import dashboardMessages, { DashboardMessage } from "./dashboard" import timeFormatMessages, { TimeFormatMessage } from "./time-format" +import helpUsMessages, { HelpUsMessage } from "./help-us" import calendarMessages, { CalendarMessage } from "@i18n/message/common/calendar" import popupDurationMessages, { PopupDurationMessage } from "@i18n/message/common/popup-duration" -import helpUsMessages, { HelpUsMessage } from "./help-us" +import buttonMessages, { ButtonMessage } from "@i18n/message/common/button" export type AppMessage = { dataManage: DataManageMessage @@ -45,6 +46,7 @@ export type AppMessage = { timeFormat: TimeFormatMessage duration: PopupDurationMessage helpUs: HelpUsMessage + button: ButtonMessage } const _default: Messages = { @@ -68,6 +70,7 @@ const _default: Messages = { timeFormat: timeFormatMessages.zh_CN, duration: popupDurationMessages.zh_CN, helpUs: helpUsMessages.zh_CN, + button: buttonMessages.zh_CN, }, zh_TW: { dataManage: dataManageMessages.zh_TW, @@ -89,6 +92,7 @@ const _default: Messages = { timeFormat: timeFormatMessages.zh_TW, duration: popupDurationMessages.zh_TW, helpUs: helpUsMessages.zh_TW, + button: buttonMessages.zh_TW, }, en: { dataManage: dataManageMessages.en, @@ -110,6 +114,7 @@ const _default: Messages = { timeFormat: timeFormatMessages.en, duration: popupDurationMessages.en, helpUs: helpUsMessages.en, + button: buttonMessages.en, }, ja: { dataManage: dataManageMessages.ja, @@ -131,6 +136,7 @@ const _default: Messages = { timeFormat: timeFormatMessages.ja, duration: popupDurationMessages.ja, helpUs: helpUsMessages.ja, + button: buttonMessages.ja, }, pt: { dataManage: dataManageMessages.pt, @@ -152,6 +158,7 @@ const _default: Messages = { timeFormat: timeFormatMessages.pt, duration: popupDurationMessages.pt, helpUs: helpUsMessages.pt, + button: buttonMessages.pt, }, } diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 5299a0e82..1904eec3c 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -12,13 +12,7 @@ "operation": "操作" }, "button": { - "add": "新增", - "test": "网址测试", - "testSimple": "测试", - "paste": "粘贴", - "save": "保存", - "delete": "删除", - "modify": "修改" + "test": "网址测试" }, "addTitle": "新增限制", "useWildcard": "是否使用通配符", @@ -34,7 +28,6 @@ "noRuleMatched": "该网址未命中任何规则", "rulesMatched": "该网址命中以下规则:" }, - "testUrlLabel": "测试网址", "urlPlaceholder": "请直接粘贴网址 ➡️" }, "zh_TW": { @@ -50,13 +43,7 @@ "operation": "操作" }, "button": { - "add": "新增", - "test": "網址測試", - "testSimple": "測試", - "paste": "粘貼", - "save": "保存", - "delete": "刪除", - "modify": "修改" + "test": "網址測試" }, "addTitle": "新增限製", "useWildcard": "是否使用通配符", @@ -72,8 +59,7 @@ "noRuleMatched": "該網址未命中任何規則", "rulesMatched": "該網址命中以下規則:" }, - "urlPlaceholder": "請直接粘貼網址 ➡️", - "testUrlLabel": "測試網址" + "urlPlaceholder": "請直接粘貼網址 ➡️" }, "en": { "conditionFilter": "URL", @@ -88,13 +74,7 @@ "operation": "Operations" }, "button": { - "add": "New", - "test": "Test URL", - "testSimple": "Test", - "paste": "Paste", - "save": "Save", - "delete": "Delete", - "modify": "Modify" + "test": "Test URL" }, "addTitle": "New", "useWildcard": "Whether to use wildcard", @@ -110,8 +90,7 @@ "noRuleMatched": "The URL does not hit any rules", "rulesMatched": "The URL hits the following rules:" }, - "urlPlaceholder": "Please paste the URL directly ➡️", - "testUrlLabel": "Test URL" + "urlPlaceholder": "Please paste the URL directly ➡️" }, "ja": { "conditionFilter": "URL", @@ -126,13 +105,7 @@ "operation": "操作" }, "button": { - "add": "新增", - "test": "テストURL", - "testSimple": "テスト", - "paste": "ペースト", - "save": "セーブ", - "delete": "削除", - "modify": "変更" + "test": "テスト URL" }, "addTitle": "新增", "useWildcard": "ワイルドカードを使用するかどうか", @@ -148,8 +121,7 @@ "noRuleMatched": "URL がどのルールとも一致しません", "rulesMatched": "URL は次のルールに一致します。" }, - "urlPlaceholder": "URLを直接貼り付けてください➡️", - "testUrlLabel": "テスト URL" + "urlPlaceholder": "URLを直接貼り付けてください➡️" }, "pt": { "conditionFilter": "URL", @@ -167,13 +139,7 @@ "operation": "Operações" }, "button": { - "add": "Criar", - "test": "Testar URL", - "paste": "Colar", - "save": "Salvar", - "delete": "Apagar", - "testSimple": "Testar", - "modify": "Modificar" + "test": "Testar URL" }, "message": { "saved": "Guardado com sucesso", @@ -186,7 +152,6 @@ "clickTestButton": "Após a entrada, por favor, clique no botão ({buttonText})", "noRuleMatched": "O URL não atinge nenhuma regra", "rulesMatched": "A URL atinge as seguintes regras:" - }, - "testUrlLabel": "Testar URL" + } } } \ No newline at end of file diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index 9fa584364..fab37bdf3 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -23,13 +23,7 @@ export type LimitMessage = { operation: string } button: { - add: string test: string - testSimple: string - paste: string - save: string - delete: string - modify: string } message: { noUrl: string @@ -43,7 +37,6 @@ export type LimitMessage = { noRuleMatched: string rulesMatched: string } - testUrlLabel: string } const _default: Messages = resource diff --git a/src/i18n/message/app/operation-resource.json b/src/i18n/message/app/operation-resource.json index 767486643..8a0726ebc 100644 --- a/src/i18n/message/app/operation-resource.json +++ b/src/i18n/message/app/operation-resource.json @@ -1,32 +1,22 @@ { "zh_CN": { "confirmTitle": "操作确认", - "successMsg": "操作成功!", - "newOne": "新增", - "save": "保存" + "successMsg": "操作成功!" }, "zh_TW": { "confirmTitle": "操作確認", - "successMsg": "操作成功!", - "newOne": "新增", - "save": "保存" + "successMsg": "操作成功!" }, "en": { - "confirmTitle": "Confirm", - "successMsg": "Successfully!", - "newOne": "New One", - "save": "Save" + "confirmTitle": "Confirmation", + "successMsg": "Successfully!" }, "ja": { "confirmTitle": "動作確認", - "successMsg": "正常に動作しました!", - "newOne": "追加", - "save": "保存" + "successMsg": "正常に動作しました!" }, "pt": { - "confirmTitle": "Confirmar", - "successMsg": "Com êxito!", - "newOne": "Criar", - "save": "Salvar" + "confirmTitle": "Confirmação de Ação", + "successMsg": "Com êxito!" } } \ No newline at end of file diff --git a/src/i18n/message/app/operation.ts b/src/i18n/message/app/operation.ts index 2a6788177..bf3f18de4 100644 --- a/src/i18n/message/app/operation.ts +++ b/src/i18n/message/app/operation.ts @@ -10,8 +10,6 @@ import resource from './operation-resource.json' export type OperationMessage = { confirmTitle: string successMsg: string - save: string - newOne: string } const _default: Messages = resource diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index aa0bf452d..8a533decb 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -1,11 +1,5 @@ { "zh_CN": { - "startDate": "开始日期", - "endDate": "结束日期", - "lastWeek": "最近一周", - "last30Days": "最近 30 天", - "today": "今天", - "yesterday": "昨天", "mergeDate": "合并日期", "mergeDomain": "合并子域名", "hostPlaceholder": "请输入域名,然后回车", @@ -32,12 +26,6 @@ } }, "zh_TW": { - "startDate": "開始日期", - "endDate": "結束日期", - "lastWeek": "最近一週", - "last30Days": "最近 30 天", - "today": "今天", - "yesterday": "昨天", "mergeDate": "合並日期", "mergeDomain": "合並子域名", "hostPlaceholder": "請輸入域名,然後回車", @@ -64,12 +52,6 @@ } }, "en": { - "startDate": "Start date", - "endDate": "End date", - "lastWeek": "Last week", - "last30Days": "Last 30 days", - "today": "Today", - "yesterday": "Yesterday", "mergeDate": "Merge date", "mergeDomain": "Merge URL", "hostPlaceholder": "Partial URL, then enter", @@ -96,12 +78,6 @@ } }, "ja": { - "startDate": "開始日", - "endDate": "終了日", - "lastWeek": "先週", - "last30Days": "過去 30 日間", - "today": "今日", - "yesterday": "昨日", "mergeDate": "マージ日", "mergeDomain": "URLをマージ", "hostPlaceholder": "URL を入力してください", @@ -128,12 +104,6 @@ } }, "pt": { - "startDate": "Data de início", - "endDate": "Data final", - "lastWeek": "Semana passada", - "last30Days": "Últimos 30 dias", - "today": "Hoje", - "yesterday": "Ontem", "mergeDate": "Mesclar Datas", "mergeDomain": "Mesclar URL", "hostPlaceholder": "URL parcial, e depois digite", diff --git a/src/i18n/message/app/report.ts b/src/i18n/message/app/report.ts index aafc0d905..a8d05255e 100644 --- a/src/i18n/message/app/report.ts +++ b/src/i18n/message/app/report.ts @@ -8,12 +8,6 @@ import resource from './report-resource.json' export type ReportMessage = { - startDate: string - endDate: string - lastWeek: string - last30Days: string - today: string - yesterday: string mergeDate: string mergeDomain: string hostPlaceholder: string diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json new file mode 100644 index 000000000..0097be621 --- /dev/null +++ b/src/i18n/message/common/button-resource.json @@ -0,0 +1,62 @@ +{ + "en": { + "create": "New", + "delete": "Delete", + "modify": "Modify", + "save": "Save", + "test": "Test", + "paste": "Paste", + "confirm": "Confirm", + "cancel": "Cancel", + "okey": "OK", + "dont": "NO!" + }, + "zh_CN": { + "create": "新增", + "delete": "删除", + "modify": "修改", + "save": "保存", + "test": "测试", + "paste": "粘贴", + "confirm": "确认", + "cancel": "取消", + "okey": "好的", + "dont": "不用了" + }, + "zh_TW": { + "create": "新增", + "delete": "刪除", + "modify": "修改", + "save": "保存", + "test": "測試", + "paste": "粘貼", + "confirm": "確認", + "cancel": "取消", + "okey": "好的", + "dont": "不用了" + }, + "ja": { + "create": "追加", + "delete": "消去", + "modify": "変更", + "save": "保存", + "test": "試す", + "paste": "貼り付ける", + "confirm": "確かめる", + "cancel": "取り消す", + "okey": "OK", + "dont": "いいえ!" + }, + "pt": { + "create": "Criar", + "delete": "Excluir", + "modify": "Modificar", + "save": "Salvar", + "test": "Ensaiar", + "paste": "Colar", + "confirm": "Confirmar", + "cancel": "Cancelar", + "okey": "OK", + "dont": "NÃO!" + } +} \ No newline at end of file diff --git a/src/i18n/message/common/button.ts b/src/i18n/message/common/button.ts new file mode 100644 index 000000000..caea7af10 --- /dev/null +++ b/src/i18n/message/common/button.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import resource from './button-resource.json' + +export type ButtonMessage = { + create: string + delete: string + modify: string + save: string + test: string + paste: string + confirm: string + cancel: string + okey: string + dont: string +} + +export default resource as Messages \ No newline at end of file diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index 4150b2544..caa08900a 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -3,30 +3,104 @@ "weekDays": "星期一|星期二|星期三|星期四|星期五|星期六|星期天", "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", "dateFormat": "{y}/{m}/{d}", - "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "开始日期", + "endDate": "结束日期" + }, + "range": { + "today": "今天", + "yesterday": "昨天", + "last24Hours": "最近 24 小时", + "last3Days": "最近 3 天", + "last7Days": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天", + "last90Days": "最近 90 天" + } }, "zh_TW": { "weekDays": "禮拜一|禮拜二|禮拜三|禮拜四|禮拜五|禮拜六|禮拜天", "months": "一月|二月|三月|四月|五月|六月|七月|八月|九月|十月|十一月|十二月", "dateFormat": "{y}/{m}/{d}", - "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "開始日期", + "endDate": "結束日期" + }, + "range": { + "today": "今天", + "yesterday": "昨天", + "last24Hours": "最近 24 小時", + "last3Days": "最近 3 天", + "last7Days": "最近 7 天", + "last15Days": "最近 15 天", + "last30Days": "最近 30 天", + "last60Days": "最近 60 天", + "last90Days": "最近 90 天" + } }, "en": { "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", "dateFormat": "{m}/{d}/{y}", - "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}" + "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}", + "label": { + "startDate": "Start date", + "endDate": "End date" + }, + "range": { + "today": "Today", + "yesterday": "Yesterday", + "last24Hours": "Last 24 hours", + "last3Days": "Last 3 days", + "last7Days": "Last 7 days", + "last15Days": "Last 15 days", + "last30Days": "Last 30 days", + "last60Days": "Last 60 days", + "last90Days": "Last 90 days" + } }, "ja": { "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Set|Oct|Nov|Dec", "dateFormat": "{y}/{m}/{d}", - "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}" + "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", + "label": { + "startDate": "開始日", + "endDate": "終了日" + }, + "range": { + "today": "今日", + "yesterday": "昨日", + "last24Hours": "過去 24 時間", + "last3Days": "過去 3 日間", + "last7Days": "過去 7 日間", + "last15Days": "過去 15 日間", + "last30Days": "過去 30 日間", + "last60Days": "過去 60 日間", + "last90Days": "過去 90 日間" + } }, "pt": { "weekDays": "Seg|Ter|Qua|Qui|Sex|Sab|Dom", "months": "Jan|Fev|Mar|Abr|Maio|Jun|Jul|Ago|Set|Out|Nov|Dez", "dateFormat": "{d}/{m}/{y}", - "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}" + "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}", + "label": { + "startDate": "Data de início", + "endDate": "Data de fim" + }, + "range": { + "today": "Hoje", + "yesterday": "Ontem", + "last3Days": "Últimos 3 dias", + "last7Days": "Últimos 7 dias", + "last15Days": "Últimos 15 dias", + "last30Days": "Últimos 30 dias", + "last60Days": "Últimos 60 dias", + "last90Days": "Últimos 90 dias" + } } } \ No newline at end of file diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index f01fa91a8..44be68735 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -12,6 +12,21 @@ export type CalendarMessage = { months: string dateFormat: string timeFormat: string + label: { + startDate: string + endDate: string + } + range: { + today: string + yesterday: string + last24Hours: string + last3Days: string + last7Days: string + last15Days: string + last30Days: string + last60Days: string + last90Days: string + } } const _default: Messages = resource From 6e6bd52dc891af4cadbce748910ffc1424c49c2b Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 17 May 2023 22:08:10 +0800 Subject: [PATCH 156/168] Display translation contributors (#212) --- src/api/crowdin.ts | 17 ++++++++++ src/app/components/help-us/index.ts | 23 +++++++------- src/app/components/help-us/member-list.ts | 37 ++++++++++++++++++++++ src/app/components/help-us/style.sass | 9 ++++++ src/i18n/message/app/help-us-resource.json | 9 ++++-- src/i18n/message/app/help-us.ts | 1 + 6 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 src/app/components/help-us/member-list.ts diff --git a/src/api/crowdin.ts b/src/api/crowdin.ts index a5299faf4..cbbdf2a1b 100644 --- a/src/api/crowdin.ts +++ b/src/api/crowdin.ts @@ -24,6 +24,12 @@ export type TranslationStatusInfo = { translationProgress: number } +export type MemberInfo = { + username: string + joinedAt: string + avatarUrl: string +} + export async function getTranslationStatus(): Promise { const limit = 500 const auth = `Bearer ${PUBLIC_TOKEN}` @@ -33,4 +39,15 @@ export async function getTranslationStatus(): Promise { }) const data: { data: { data: TranslationStatusInfo }[] } = response.data return data.data.map(i => i.data) +} + +export async function getMembers(): Promise { + const limit = 20 + const auth = `Bearer ${PUBLIC_TOKEN}` + const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/members?limit=${limit}` + const response: AxiosResponse = await axios.get(url, { + headers: { "Authorization": auth } + }) + const data: { data: { data: MemberInfo }[] } = response.data + return data.data.map(i => i.data) } \ No newline at end of file diff --git a/src/app/components/help-us/index.ts b/src/app/components/help-us/index.ts index 3f85500de..600ac4fac 100644 --- a/src/app/components/help-us/index.ts +++ b/src/app/components/help-us/index.ts @@ -1,21 +1,22 @@ import { ElCard } from "element-plus" import { defineComponent, h } from "vue" import ContentContainer from "../common/content-container" -import HelpUsAlertInfo from "./alert-info" -import HelpUsToolbar from "./toolbar" -import HelpUsProgressList from "./progress-list" +import AlertInfo from "./alert-info" +import Toolbar from "./toolbar" +import ProgressList from "./progress-list" +import MemberList from "./member-list" import "./style" -const _default = defineComponent({ - name: "HelpUs", - render: () => h(ContentContainer, () => h(ElCard, +const _default = defineComponent(() => + () => h(ContentContainer, () => h(ElCard, { class: 'help-us' }, () => [ - h(HelpUsAlertInfo), - h(HelpUsToolbar), - h(HelpUsProgressList), + h(AlertInfo), + h(Toolbar), + h(ProgressList), + h(MemberList), ]) - ), -}) + ) +) export default _default \ No newline at end of file diff --git a/src/app/components/help-us/member-list.ts b/src/app/components/help-us/member-list.ts new file mode 100644 index 000000000..bdbcf544b --- /dev/null +++ b/src/app/components/help-us/member-list.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { getMembers, MemberInfo } from "@api/crowdin" +import { t } from "@app/locale" +import { ElDivider } from "element-plus" +import { defineComponent, h, onMounted, Ref, ref } from "vue" + +async function queryData(listRef: Ref) { + const list = await getMembers() || [] + list.sort((a, b) => (a.joinedAt || "").localeCompare(b.joinedAt || "")) + listRef.value = list +} + +function renderMember({ avatarUrl, username }: MemberInfo) { + const img = h('img', { src: avatarUrl, alt: username, title: username }) + const url = `https://crowdin.com/profile/${username}` + return h('a', { href: url, target: '_blank' }, img) +} + +const _default = defineComponent({ + name: 'HelpUsProgressList', + setup() { + const list: Ref = ref([]) + onMounted(() => queryData(list)) + return () => h('div', { class: 'member-container' }, [ + h(ElDivider, {}, () => t(msg => msg.helpUs.contributors)), + h('div', list.value.map(renderMember)) + ]) + }, +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/help-us/style.sass b/src/app/components/help-us/style.sass index 29e070dc7..88d548163 100644 --- a/src/app/components/help-us/style.sass +++ b/src/app/components/help-us/style.sass @@ -3,6 +3,15 @@ margin: 30px 0 width: 100% display: flex + .member-container + margin-top: 10px + text-align: center + img + width: 60px + height: 60px + border-radius: 30px + a:not(:last-child) + margin-right: 15px .progress-container display: flex flex-direction: row diff --git a/src/i18n/message/app/help-us-resource.json b/src/i18n/message/app/help-us-resource.json index 241387cf9..ccaa4a9dd 100644 --- a/src/i18n/message/app/help-us-resource.json +++ b/src/i18n/message/app/help-us-resource.json @@ -8,7 +8,8 @@ "l4": "当某种语言的翻译进度达到 50% 之后,我将会考虑在扩展中支持它。" }, "button": "前往 Crowdin", - "loading": "正在查询翻译进度..." + "loading": "正在查询翻译进度...", + "contributors": "贡献者名单" }, "en": { "title": "Feel free to help improve the extension's localization translations!", @@ -19,10 +20,12 @@ "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..." + "loading": "Checking translation progress...", + "contributors": "Contributor List" }, "zh_TW": { "button": "前往 Crowdin", - "loading": "正在檢查翻譯進度..." + "loading": "正在檢查翻譯進度...", + "contributors": "貢獻者名單" } } \ No newline at end of file diff --git a/src/i18n/message/app/help-us.ts b/src/i18n/message/app/help-us.ts index ca8427e82..dc0851a85 100644 --- a/src/i18n/message/app/help-us.ts +++ b/src/i18n/message/app/help-us.ts @@ -18,6 +18,7 @@ export type HelpUsMessage = { alert: { [line in _AlertLine]: string } button: string loading: string + contributors: string } const _default: Messages = resource From 5dfc78f08ca6d3422f0365f6acd3df3adc7bc216 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 17 May 2023 22:17:08 +0800 Subject: [PATCH 157/168] Fix bgcolor of divider --- src/app/styles/compatible.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/styles/compatible.sass b/src/app/styles/compatible.sass index 58f1ad715..c11b71959 100644 --- a/src/app/styles/compatible.sass +++ b/src/app/styles/compatible.sass @@ -46,7 +46,8 @@ margin-left: 7px text-align: center font-size: 10px - +.el-divider__text + background-color: transparent \:root --el-input-height: 40px --el-input-inner-height: 38px From 7f7e4ef1e505596a6b287304a0d5babe6d941acd Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 17 May 2023 22:46:18 +0800 Subject: [PATCH 158/168] pt -> pt_PT (#212) --- script/crowdin/common.ts | 6 +-- src/app/components/help-us/progress-list.ts | 2 +- .../report/table/columns/operation.ts | 2 +- src/app/index.ts | 2 +- src/guide/index.ts | 2 +- src/i18n/chrome/index.ts | 2 +- src/i18n/chrome/message.ts | 10 ++--- src/i18n/index.ts | 7 ++-- src/i18n/message/app/analysis-resource.json | 2 +- src/i18n/message/app/confirm-resource.json | 2 +- src/i18n/message/app/dashboard-resource.json | 2 +- .../message/app/data-manage-resource.json | 2 +- src/i18n/message/app/habit-resource.json | 2 +- src/i18n/message/app/index.ts | 42 +++++++++---------- src/i18n/message/app/limit-resource.json | 2 +- src/i18n/message/app/menu-resource.json | 2 +- src/i18n/message/app/merge-rule-resource.json | 2 +- src/i18n/message/app/operation-resource.json | 2 +- src/i18n/message/app/option-resource.json | 2 +- src/i18n/message/app/report-resource.json | 2 +- .../message/app/site-manage-resource.json | 2 +- .../message/app/time-format-resource.json | 2 +- src/i18n/message/app/whitelist-resource.json | 2 +- src/i18n/message/common/base-resource.json | 2 +- src/i18n/message/common/button-resource.json | 2 +- .../message/common/calendar-resource.json | 2 +- .../common/content-script-resource.json | 2 +- .../common/context-menus-resource.json | 2 +- src/i18n/message/common/initial-resource.json | 2 +- src/i18n/message/common/item-resource.json | 2 +- src/i18n/message/common/locale-resource.json | 2 +- src/i18n/message/common/merge-resource.json | 2 +- src/i18n/message/common/meta-resource.json | 2 +- .../common/popup-duration-resource.json | 2 +- src/i18n/message/guide/app-resource.json | 2 +- src/i18n/message/guide/backup-resource.json | 2 +- src/i18n/message/guide/home-resource.json | 2 +- src/i18n/message/guide/index.ts | 28 ++++++------- src/i18n/message/guide/layout-resource.json | 2 +- src/i18n/message/guide/limit-resource.json | 2 +- src/i18n/message/guide/merge-resource.json | 2 +- src/i18n/message/guide/privacy-resource.json | 2 +- src/i18n/message/guide/start-resource.json | 2 +- src/i18n/message/guide/virtual-resource.json | 2 +- src/i18n/message/popup/chart-resource.json | 2 +- src/i18n/message/popup/index.ts | 12 +++--- src/util/constant/url.ts | 2 +- types/timer/index.d.ts | 2 +- 48 files changed, 95 insertions(+), 94 deletions(-) diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index cb6614a93..004a475c9 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -11,7 +11,7 @@ export const SOURCE_LOCALE: timer.SourceLocale = 'en' export const ALL_TRANS_LOCALES: timer.Locale[] = [ 'ja', 'zh_TW', - 'pt', + 'pt_PT', ] const CROWDIN_I18N_MAP: Record = { @@ -19,7 +19,7 @@ const CROWDIN_I18N_MAP: Record = { ja: 'ja', 'zh-CN': 'zh_CN', 'zh-TW': 'zh_TW', - 'pt-PT': 'pt', + 'pt-PT': 'pt_PT', } const I18N_CROWDIN_MAP: Record = { @@ -27,7 +27,7 @@ const I18N_CROWDIN_MAP: Record = { ja: 'ja', zh_CN: 'zh-CN', zh_TW: 'zh-TW', - pt: 'pt-PT' + pt_PT: 'pt-PT' } export const crowdinLangOf = (locale: timer.Locale) => I18N_CROWDIN_MAP[locale] diff --git a/src/app/components/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts index beb02d681..e1a53e2f6 100644 --- a/src/app/components/help-us/progress-list.ts +++ b/src/app/components/help-us/progress-list.ts @@ -22,7 +22,7 @@ const localeCrowdMap: { [locale in SupportedLocale]: string } = { es: "es-ES", ko: "ko", pl: "pl", - pt: "pt-PT", + pt_PT: "pt-PT", ru: "ru", uk: "uk", fr: "fr", diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index 4f396f2f2..758b4f01f 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -48,7 +48,7 @@ const LOCALE_WIDTH: { [locale in timer.Locale]: number } = { zh_CN: 290, ja: 360, zh_TW: 290, - pt: 400, + pt_PT: 340, } const _default = defineComponent({ name: "OperationColumn", diff --git a/src/app/index.ts b/src/app/index.ts index e53ef4e7a..e344738ef 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -25,7 +25,7 @@ const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), en: () => import('element-plus/lib/locale/lang/en'), ja: () => import('element-plus/lib/locale/lang/ja'), - pt: () => import('element-plus/lib/locale/lang/pt'), + pt_PT: () => import('element-plus/lib/locale/lang/pt'), } async function main() { diff --git a/src/guide/index.ts b/src/guide/index.ts index 9b30f67e9..0c6fc7c9c 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -20,7 +20,7 @@ const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), en: () => import('element-plus/lib/locale/lang/en'), ja: () => import('element-plus/lib/locale/lang/ja'), - pt: () => import('element-plus/lib/locale/lang/pt'), + pt_PT: () => import('element-plus/lib/locale/lang/pt'), } async function main() { diff --git a/src/i18n/chrome/index.ts b/src/i18n/chrome/index.ts index 8b5ededef..a4e9fc222 100644 --- a/src/i18n/chrome/index.ts +++ b/src/i18n/chrome/index.ts @@ -18,7 +18,7 @@ const _default: { [locale in FakedLocale]: any } = { zh_TW: compile(messages.zh_TW), en: compile(messages.en), ja: compile(messages.ja), - pt: compile(messages.pt) + pt_PT: compile(messages.pt_PT) } export default _default \ No newline at end of file diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index a3567a5d9..7fde80765 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -42,11 +42,11 @@ const messages: Messages = { contextMenus: contextMenusMessages.ja, initial: initialMessages.ja, }, - pt: { - meta: metaMessages.pt, - base: baseMessages.pt, - contextMenus: contextMenusMessages.pt, - initial: initialMessages.pt, + pt_PT: { + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, + contextMenus: contextMenusMessages.pt_PT, + initial: initialMessages.pt_PT, } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index fa829c835..846492bb2 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -16,7 +16,7 @@ export type FakedLocale = timer.Locale */ const FEEDBACK_LOCALE: timer.Locale = "en" -export const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja', 'pt'] +export const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja', 'pt_PT'] export const defaultLocale: timer.Locale = "zh_CN" @@ -28,8 +28,9 @@ const chrome2I18n: { [key: string]: timer.Locale } = { 'en-US': "en", 'en-GB': "en", 'ja': "ja", - 'pt-PT': 'pt', - 'pt-BR': 'pt', + 'pt': 'pt_PT', + 'pt-PT': 'pt_PT', + 'pt-BR': 'pt_PT', } const translationChrome2I18n: { [key: string]: timer.TranslatingLocale } = { diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json index 0a1edb533..47680d249 100644 --- a/src/i18n/message/app/analysis-resource.json +++ b/src/i18n/message/app/analysis-resource.json @@ -107,7 +107,7 @@ "visitTitle": "訪問数の傾向" } }, - "pt": { + "pt_PT": { "common": { "focusTotal": "Tempo total de navegação", "visitTotal": "Total de visitas", diff --git a/src/i18n/message/app/confirm-resource.json b/src/i18n/message/app/confirm-resource.json index 87477087c..ecd88c5bc 100644 --- a/src/i18n/message/app/confirm-resource.json +++ b/src/i18n/message/app/confirm-resource.json @@ -15,7 +15,7 @@ "confirmMsg": "OK", "cancelMsg": "キャンセル" }, - "pt": { + "pt_PT": { "confirmMsg": "OK", "cancelMsg": "Não!" } diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json index 034d203e2..12c6539e2 100644 --- a/src/i18n/message/app/dashboard-resource.json +++ b/src/i18n/message/app/dashboard-resource.json @@ -103,7 +103,7 @@ "decline": "減らす" } }, - "pt": { + "pt_PT": { "heatMap": { "title0": "Navegou mais de {hour} horas no último ano", "title1": "Navegue por menos de 1 hora no ano passado", diff --git a/src/i18n/message/app/data-manage-resource.json b/src/i18n/message/app/data-manage-resource.json index f568cd871..0de06be0e 100644 --- a/src/i18n/message/app/data-manage-resource.json +++ b/src/i18n/message/app/data-manage-resource.json @@ -87,7 +87,7 @@ "importError": "ファイル形式エラー", "migrated": "正常にインポートされました" }, - "pt": { + "pt_PT": { "totalMemoryAlert": "O navegador fornece {size}MB para armazenar dados locais para cada extensão", "totalMemoryAlert1": "Não é possível determinar a memória máxima disponível permitida pelo navegador", "usedMemoryAlert": "{size}MB está atualmente em uso", diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json index 8a8170063..371c723ad 100644 --- a/src/i18n/message/app/habit-resource.json +++ b/src/i18n/message/app/habit-resource.json @@ -67,7 +67,7 @@ "yAxisHour": "閲覧時間/時間" } }, - "pt": { + "pt_PT": { "sizes": { "fifteen": "Por 15 minutos", "halfHour": "Por meia hora", diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 26be11bea..086113dc6 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -138,27 +138,27 @@ const _default: Messages = { helpUs: helpUsMessages.ja, button: buttonMessages.ja, }, - pt: { - dataManage: dataManageMessages.pt, - item: itemMessages.pt, - mergeCommon: mergeCommonMessages.pt, - report: reportMessages.pt, - whitelist: whitelistMessages.pt, - mergeRule: mergeRuleMessages.pt, - option: optionMessages.pt, - analysis: analysisMessages.pt, - menu: menuMessages.pt, - habit: habitMessages.pt, - limit: limitMessages.pt, - siteManage: siteManageManages.pt, - operation: operationMessages.pt, - confirm: confirmMessages.pt, - dashboard: dashboardMessages.pt, - calendar: calendarMessages.pt, - timeFormat: timeFormatMessages.pt, - duration: popupDurationMessages.pt, - helpUs: helpUsMessages.pt, - button: buttonMessages.pt, + pt_PT: { + dataManage: dataManageMessages.pt_PT, + item: itemMessages.pt_PT, + mergeCommon: mergeCommonMessages.pt_PT, + report: reportMessages.pt_PT, + whitelist: whitelistMessages.pt_PT, + mergeRule: mergeRuleMessages.pt_PT, + option: optionMessages.pt_PT, + analysis: analysisMessages.pt_PT, + menu: menuMessages.pt_PT, + habit: habitMessages.pt_PT, + limit: limitMessages.pt_PT, + siteManage: siteManageManages.pt_PT, + operation: operationMessages.pt_PT, + confirm: confirmMessages.pt_PT, + dashboard: dashboardMessages.pt_PT, + calendar: calendarMessages.pt_PT, + timeFormat: timeFormatMessages.pt_PT, + duration: popupDurationMessages.pt_PT, + helpUs: helpUsMessages.pt_PT, + button: buttonMessages.pt_PT, }, } diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 1904eec3c..4af1ed5bd 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -123,7 +123,7 @@ }, "urlPlaceholder": "URLを直接貼り付けてください➡️" }, - "pt": { + "pt_PT": { "conditionFilter": "URL", "filterDisabled": "Apenas Habilitadas", "addTitle": "Criar", diff --git a/src/i18n/message/app/menu-resource.json b/src/i18n/message/app/menu-resource.json index 71b21f7f1..b27b9bf38 100644 --- a/src/i18n/message/app/menu-resource.json +++ b/src/i18n/message/app/menu-resource.json @@ -79,7 +79,7 @@ "helpUs": "協力する", "userManual": "ユーザーマニュアル" }, - "pt": { + "pt_PT": { "dashboard": "Painel de Controlo", "data": "Os Meus Dados", "dataReport": "Registo", diff --git a/src/i18n/message/app/merge-rule-resource.json b/src/i18n/message/app/merge-rule-resource.json index e977702a4..6ba2fa4f8 100644 --- a/src/i18n/message/app/merge-rule-resource.json +++ b/src/i18n/message/app/merge-rule-resource.json @@ -59,7 +59,7 @@ "infoAlert4": "記入しない場合は、元のドメイン名が統合されないことを意味します", "infoAlert5": "一致するルールがない場合、デフォルトで {psl} より前のレベルになります" }, - "pt": { + "pt_PT": { "removeConfirmMsg": "{origin} será removido das regras personalizadas de mesclagem.", "originPlaceholder": "Site original", "mergedPlaceholder": "Mesclado", diff --git a/src/i18n/message/app/operation-resource.json b/src/i18n/message/app/operation-resource.json index 8a0726ebc..03757ace3 100644 --- a/src/i18n/message/app/operation-resource.json +++ b/src/i18n/message/app/operation-resource.json @@ -15,7 +15,7 @@ "confirmTitle": "動作確認", "successMsg": "正常に動作しました!" }, - "pt": { + "pt_PT": { "confirmTitle": "Confirmação de Ação", "successMsg": "Com êxito!" } diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index a944f66ef..036125a4c 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -335,7 +335,7 @@ "resetSuccess": "デフォルトに正常にリセット", "defaultValue": "デフォルト値:{default}" }, - "pt": { + "pt_PT": { "yes": "Sim", "no": "Não", "resetButton": "Reiniciar", diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index 8a533decb..e50886d1e 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -103,7 +103,7 @@ } } }, - "pt": { + "pt_PT": { "mergeDate": "Mesclar Datas", "mergeDomain": "Mesclar URL", "hostPlaceholder": "URL parcial, e depois digite", diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index c19f2dc7d..becce9ebf 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -190,7 +190,7 @@ } } }, - "pt": { + "pt_PT": { "hostPlaceholder": "URL parcial, e depois digite", "aliasPlaceholder": "Nome parcial, e depois digite", "onlyDetected": "Apenas detetado", diff --git a/src/i18n/message/app/time-format-resource.json b/src/i18n/message/app/time-format-resource.json index 264df1295..066590f8d 100644 --- a/src/i18n/message/app/time-format-resource.json +++ b/src/i18n/message/app/time-format-resource.json @@ -23,7 +23,7 @@ "minute": "分単位で表示", "second": "秒単位で表示" }, - "pt": { + "pt_PT": { "default": "Formato padrão da hora", "hour": "Exibir em horas", "minute": "Exibir em minutos", diff --git a/src/i18n/message/app/whitelist-resource.json b/src/i18n/message/app/whitelist-resource.json index 3891ffda5..e43f3d655 100644 --- a/src/i18n/message/app/whitelist-resource.json +++ b/src/i18n/message/app/whitelist-resource.json @@ -39,7 +39,7 @@ "placeholder": "URL", "errorInput": "無効なURL" }, - "pt": { + "pt_PT": { "addConfirmMsg": "{url} não será mais contado após adicionado à lista ignorada.", "removeConfirmMsg": "{url} será removido da lista ignorada.", "duplicateMsg": "Duplicado", diff --git a/src/i18n/message/common/base-resource.json b/src/i18n/message/common/base-resource.json index 251738989..012a7cdce 100644 --- a/src/i18n/message/common/base-resource.json +++ b/src/i18n/message/common/base-resource.json @@ -19,7 +19,7 @@ "allFunction": "すべての機能", "guidePage": "ユーザーマニュアル" }, - "pt": { + "pt_PT": { "currentVersion": "Versão: {version}", "allFunction": "Todas as Funções", "guidePage": "Manual do Utilizador" diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json index 0097be621..d8610a901 100644 --- a/src/i18n/message/common/button-resource.json +++ b/src/i18n/message/common/button-resource.json @@ -47,7 +47,7 @@ "okey": "OK", "dont": "いいえ!" }, - "pt": { + "pt_PT": { "create": "Criar", "delete": "Excluir", "modify": "Modificar", diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index caa08900a..74df2886c 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -83,7 +83,7 @@ "last90Days": "過去 90 日間" } }, - "pt": { + "pt_PT": { "weekDays": "Seg|Ter|Qua|Qui|Sex|Sab|Dom", "months": "Jan|Fev|Mar|Abr|Maio|Jun|Jul|Ago|Set|Out|Nov|Dez", "dateFormat": "{d}/{m}/{y}", diff --git a/src/i18n/message/common/content-script-resource.json b/src/i18n/message/common/content-script-resource.json index b941b14c4..15fa5fade 100644 --- a/src/i18n/message/common/content-script-resource.json +++ b/src/i18n/message/common/content-script-resource.json @@ -35,7 +35,7 @@ "timeLimitMsg": "【{appName}】によって制限されています", "more5Minutes": "さらに5分間見てください! ! 約束します!" }, - "pt": { + "pt_PT": { "closeAlert": "Você pode desligar as dicas acima na opção do {appName}!", "timeWithHour": "{hour} horas {minute} minutos {second} segundos", "timeWithMinute": "{minute} minuto(s) {second} segundo(s)", diff --git a/src/i18n/message/common/context-menus-resource.json b/src/i18n/message/common/context-menus-resource.json index 0984c414f..c3811e7b8 100644 --- a/src/i18n/message/common/context-menus-resource.json +++ b/src/i18n/message/common/context-menus-resource.json @@ -27,7 +27,7 @@ "repoPage": "ソースコード", "feedbackPage": "フィードバックの欠如" }, - "pt": { + "pt_PT": { "add2Whitelist": "Ignorar {host}", "removeFromWhitelist": "Habilitar {host}", "optionPage": "Opções", diff --git a/src/i18n/message/common/initial-resource.json b/src/i18n/message/common/initial-resource.json index 7c7d56e46..3481c65b6 100644 --- a/src/i18n/message/common/initial-resource.json +++ b/src/i18n/message/common/initial-resource.json @@ -31,7 +31,7 @@ "txt": "TXT" } }, - "pt": { + "pt_PT": { "localFile": { "json": "Arquivos JSON", "pdf": "Arquivos PDF", diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index af2fffaa5..dbfad7db3 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -71,7 +71,7 @@ "importWholeData": "書き出す" } }, - "pt": { + "pt_PT": { "date": "Data", "host": "URL do Site", "focus": "Tempo de Navegação", diff --git a/src/i18n/message/common/locale-resource.json b/src/i18n/message/common/locale-resource.json index 4d3468e36..10e83ad2f 100644 --- a/src/i18n/message/common/locale-resource.json +++ b/src/i18n/message/common/locale-resource.json @@ -15,7 +15,7 @@ "name": "日本語", "comma": "、" }, - "pt": { + "pt_PT": { "name": "Português", "comma": "," }, diff --git a/src/i18n/message/common/merge-resource.json b/src/i18n/message/common/merge-resource.json index 809d51829..be8179dad 100644 --- a/src/i18n/message/common/merge-resource.json +++ b/src/i18n/message/common/merge-resource.json @@ -23,7 +23,7 @@ "level": "{level} 次ドメイン" } }, - "pt": { + "pt_PT": { "tagResult": { "blank": "Não Mesclar", "level": "Mantenha o Nível {level}" diff --git a/src/i18n/message/common/meta-resource.json b/src/i18n/message/common/meta-resource.json index 6c783b5f5..0ac02eeff 100644 --- a/src/i18n/message/common/meta-resource.json +++ b/src/i18n/message/common/meta-resource.json @@ -21,7 +21,7 @@ "description": "To be the BEST webtime tracker.", "slogan": "Insight & Improve" }, - "pt": { + "pt_PT": { "name": "Rastreador de Tempo", "marketName": "Rastreador de Tempo", "description": "Ser o MELHOR rastreador de tempo da web." diff --git a/src/i18n/message/common/popup-duration-resource.json b/src/i18n/message/common/popup-duration-resource.json index 9b3b912b0..14116abb8 100644 --- a/src/i18n/message/common/popup-duration-resource.json +++ b/src/i18n/message/common/popup-duration-resource.json @@ -23,7 +23,7 @@ "thisMonth": "今月の", "last30Days": "過去 30 日間" }, - "pt": { + "pt_PT": { "today": "Hoje", "thisWeek": "Esta Semana", "thisMonth": "Este Mês", diff --git a/src/i18n/message/guide/app-resource.json b/src/i18n/message/guide/app-resource.json index 99920ae18..cc60bfd64 100644 --- a/src/i18n/message/guide/app-resource.json +++ b/src/i18n/message/guide/app-resource.json @@ -27,7 +27,7 @@ "l2": "アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。", "p2": "ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。" }, - "pt": { + "pt_PT": { "title": "Digite a página de gestão", "p1": "Com base nos ícones, a extensão fornece uma maneira mais conveniente de ver dados. Mas se quiser experimentar todas as funcionalidades, precisa visitar a página de gestão da extensão, por uma das duas seguintes maneiras.", "l1": "Pode clicar com o botão direito do rato no ícone da extensão e clicar em [{button}] no ementa pop-up.", diff --git a/src/i18n/message/guide/backup-resource.json b/src/i18n/message/guide/backup-resource.json index fe4e598aa..c4e8a5063 100644 --- a/src/i18n/message/guide/backup-resource.json +++ b/src/i18n/message/guide/backup-resource.json @@ -75,7 +75,7 @@ "tip": "リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。" } }, - "pt": { + "pt_PT": { "title": "Backup dos dados com GitHub Gist", "p1": "Esta extensão permite aos utilizadores fazer backup de dados usando o GitHub Gist{link} através de configurações simples.", "upload": { diff --git a/src/i18n/message/guide/home-resource.json b/src/i18n/message/guide/home-resource.json index 6789c2069..c9300c1dd 100644 --- a/src/i18n/message/guide/home-resource.json +++ b/src/i18n/message/guide/home-resource.json @@ -19,7 +19,7 @@ "button": "すぐに始めましょう", "download": "{browser} にインストール" }, - "pt": { + "pt_PT": { "desc": "O {appName} pode ajudá-lo a controlar o tempo gasto em sites de navegação e a contagem de visitas, com o que pode perceber e melhorar os seus hábitos na web.", "button": "Comece agora!", "download": "Instalar para {browser}" diff --git a/src/i18n/message/guide/index.ts b/src/i18n/message/guide/index.ts index 7c98e5f3f..72463d00c 100644 --- a/src/i18n/message/guide/index.ts +++ b/src/i18n/message/guide/index.ts @@ -96,20 +96,20 @@ const _default: Messages = { backup: backupMessages.ja, appMenu: appMenuMessages.ja, }, - pt: { - mergeCommon: mergeCommonMessages.pt, - layout: layoutMessages.pt, - home: homeMessages.pt, - start: startMessages.pt, - privacy: privacyMessages.pt, - meta: metaMessages.pt, - base: baseMessages.pt, - app: appMessages.pt, - merge: mergeMessages.pt, - virtual: virtualMessages.pt, - limit: limitMessages.pt, - backup: backupMessages.pt, - appMenu: appMenuMessages.pt, + pt_PT: { + mergeCommon: mergeCommonMessages.pt_PT, + layout: layoutMessages.pt_PT, + home: homeMessages.pt_PT, + start: startMessages.pt_PT, + privacy: privacyMessages.pt_PT, + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, + app: appMessages.pt_PT, + merge: mergeMessages.pt_PT, + virtual: virtualMessages.pt_PT, + limit: limitMessages.pt_PT, + backup: backupMessages.pt_PT, + appMenu: appMenuMessages.pt_PT, }, } diff --git a/src/i18n/message/guide/layout-resource.json b/src/i18n/message/guide/layout-resource.json index 341c0dc3b..974e389f0 100644 --- a/src/i18n/message/guide/layout-resource.json +++ b/src/i18n/message/guide/layout-resource.json @@ -35,7 +35,7 @@ "usage": "高度な使い方" } }, - "pt": { + "pt_PT": { "header": { "sourceCode": "Exibir código-fonte", "email": "Autor do contacto" diff --git a/src/i18n/message/guide/limit-resource.json b/src/i18n/message/guide/limit-resource.json index cc9a39f41..06a1a71c7 100644 --- a/src/i18n/message/guide/limit-resource.json +++ b/src/i18n/message/guide/limit-resource.json @@ -43,7 +43,7 @@ "check": "最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。" } }, - "pt": { + "pt_PT": { "title": "Limitar tempo de navegação de todos os dias", "p1": "Se deseja limitar o tempo de navegação de certas URL por dia, pode fazê-lo criando uma regra diária de limite de tempo.", "step": { diff --git a/src/i18n/message/guide/merge-resource.json b/src/i18n/message/guide/merge-resource.json index 4fd4ebaa7..239b67bde 100644 --- a/src/i18n/message/guide/merge-resource.json +++ b/src/i18n/message/guide/merge-resource.json @@ -127,7 +127,7 @@ "hitCol": "ヒットルール" } }, - "pt": { + "pt_PT": { "title": "Resumir dados de sites relacionados", "p1": "Esta extensão é contada pelo nome de domínio, por exemplo, {demo1} e {demo2} serão contados como 2 registos. Se quer ver os dados agregados para ambos os sites, precisará usar o recurso de mesclagem.", "p2": "Na maioria das páginas de exibição de dados, são suportadas consultas mescladas. E os utilizadores podem personalizar as regras de mesclagem na página de segundo plano{link}.", diff --git a/src/i18n/message/guide/privacy-resource.json b/src/i18n/message/guide/privacy-resource.json index 306762d15..4526d6add 100644 --- a/src/i18n/message/guide/privacy-resource.json +++ b/src/i18n/message/guide/privacy-resource.json @@ -135,7 +135,7 @@ "p3": "私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません." } }, - "pt": { + "pt_PT": { "title": "Declaração de privacidade", "alert": "Para fornecer serviços completos, esta extensão irá necessariamente coletar alguns dos seus dados pessoais durante o uso, consulte a seguinte declaração de privacidade para obter detalhes.", "scope": { diff --git a/src/i18n/message/guide/start-resource.json b/src/i18n/message/guide/start-resource.json index 946c1535d..ea4eb771a 100644 --- a/src/i18n/message/guide/start-resource.json +++ b/src/i18n/message/guide/start-resource.json @@ -43,7 +43,7 @@ "s3p1": "最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。", "alert": "基本的な使い方を学んだので、試してみてください!!" }, - "pt": { + "pt_PT": { "title": "Iniciar", "p1": "Pode começar rapidamente a usar essa extensão em apenas 3 etapas fáceis.", "s1": "1. Fixar o ícone", diff --git a/src/i18n/message/guide/virtual-resource.json b/src/i18n/message/guide/virtual-resource.json index 7cb72c62d..c038be51c 100644 --- a/src/i18n/message/guide/virtual-resource.json +++ b/src/i18n/message/guide/virtual-resource.json @@ -43,7 +43,7 @@ "browse": "最後に、関連する URL を参照してデータを観察します。" } }, - "pt": { + "pt_PT": { "title": "Contar dados para URL específicos", "p1": "Se deseja contar o tempo de navegação e o número de visitas de certas URL, não apenas o nome de domínio, pode fazê-lo criando um site virtual.", "step": { diff --git a/src/i18n/message/popup/chart-resource.json b/src/i18n/message/popup/chart-resource.json index 7cd1b3f5b..eafa8ec88 100644 --- a/src/i18n/message/popup/chart-resource.json +++ b/src/i18n/message/popup/chart-resource.json @@ -83,7 +83,7 @@ "updateVersionInfo": "最新バージョン:{version}", "updateVersionInfo4Firefox": "管理ページで {version} にアップグレードしてください" }, - "pt": { + "pt_PT": { "mergeHostLabel": "Mesclar Sítios Web", "fileName": "Lista_de_Horários_da_Web_{today}_por_{app}", "saveAsImageTitle": "Capturas", diff --git a/src/i18n/message/popup/index.ts b/src/i18n/message/popup/index.ts index b2d94b05f..aff4391e6 100644 --- a/src/i18n/message/popup/index.ts +++ b/src/i18n/message/popup/index.ts @@ -48,12 +48,12 @@ const _default: Messages = { meta: metaMessages.ja, base: baseMessages.ja, }, - pt: { - chart: chartMessages.pt, - duration: popupDurationMessages.pt, - item: itemMessages.pt, - meta: metaMessages.pt, - base: baseMessages.pt, + pt_PT: { + chart: chartMessages.pt_PT, + duration: popupDurationMessages.pt_PT, + item: itemMessages.pt_PT, + meta: metaMessages.pt_PT, + base: baseMessages.pt_PT, }, } diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 188981a0a..5b8017062 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -67,7 +67,7 @@ export const UNINSTALL_QUESTIONNAIRE: { [locale in timer.Locale]: string } = { zh_TW: 'https://docs.google.com/forms/d/e/1FAIpQLSdK93q-548dK-2naoS3DaArdc7tEGoUY9JQvaXP5Kpov8h6-A/viewform?usp=sf_link', ja: 'https://docs.google.com/forms/d/e/1FAIpQLSdsB3onZuleNf6j7KJJLbcote647WV6yeUr-9m7Db5QXakfpg/viewform?usp=sf_link', en: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', - pt: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', + pt_PT: 'https://docs.google.com/forms/d/e/1FAIpQLSflhZAFTw1rTUjAEwgxqCaBuhLBBthwEK9fIjvmwWfITLSK9A/viewform?usp=sf_link', } /** diff --git a/types/timer/index.d.ts b/types/timer/index.d.ts index c9974ba21..63caea4c2 100644 --- a/types/timer/index.d.ts +++ b/types/timer/index.d.ts @@ -16,7 +16,7 @@ declare namespace timer { // @since 0.9.0 | 'zh_TW' // @since 1.8.2 - | 'pt' + | 'pt_PT' /** * @since 0.8.0 */ From f2e3e80b51bbdf1e73afe14a263f2febaf38ff35 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 20 May 2023 15:32:57 +0800 Subject: [PATCH 159/168] v1.8.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 31ba7eacd..6ba7adee9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.8.1", + "version": "1.8.2", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -68,4 +68,4 @@ "engines": { "node": ">=16" } -} +} \ No newline at end of file From 679aab27b1cf11b6acb7cbe4305d8939d9979e6b Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 20 May 2023 15:37:31 +0800 Subject: [PATCH 160/168] Fix the homepage of Firefox (#213) --- src/util/constant/url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index 5b8017062..300753ab0 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -8,7 +8,7 @@ import { getRuntimeId } from "@api/chrome/runtime" import { IS_FIREFOX, IS_CHROME, IS_EDGE } from "./environment" -export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/firefox/addon/web%E6%99%82%E9%96%93%E7%B5%B1%E8%A8%88/' +export const FIREFOX_HOMEPAGE = 'https://addons.mozilla.org/firefox/addon/besttimetracker' export const CHROME_HOMEPAGE = 'https://chrome.google.com/webstore/detail/%E7%BD%91%E8%B4%B9%E5%BE%88%E8%B4%B5-%E4%B8%8A%E7%BD%91%E6%97%B6%E9%97%B4%E7%BB%9F%E8%AE%A1/dkdhhcbjijekmneelocdllcldcpmekmm' export const EDGE_HOMEPAGE = 'https://microsoftedge.microsoft.com/addons/detail/timer-the-web-time-is-e/fepjgblalcnepokjblgbgmapmlkgfahc' From f113e45436acd719d4ada699947548c9583c3819 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 20 May 2023 15:40:02 +0800 Subject: [PATCH 161/168] Fix clean button error (#211) --- src/app/components/limit/modify/style/el-input.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/limit/modify/style/el-input.sass b/src/app/components/limit/modify/style/el-input.sass index c17ef872a..b9d83e895 100644 --- a/src/app/components/limit/modify/style/el-input.sass +++ b/src/app/components/limit/modify/style/el-input.sass @@ -19,7 +19,7 @@ $prefixWidth: 110px // Fix shaking of clear button // @see https://github.com/sheepzh/timer/issues/123 position: absolute - right: 59px + right: 12px .el-input-group__append width: 12px .el-input__prefix From 9c78213dfc778c9399dbb78d75898549441fbdf5 Mon Sep 17 00:00:00 2001 From: ZHY Date: Wed, 14 Jun 2023 17:37:49 +0800 Subject: [PATCH 162/168] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 17042beb2..f0d868b1b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Timer can - support to export files formatted _.csv_ and _.json_. - support dark mode. - support to sync data with GitHub Gist across serveral browser clients. +- support to count the time of any URLs described by Ant Expressions, e.g. github.com/sheepzh/timer/** ## Install From 3f43a49d409db2c6a8e21d0bfde95dd99c5f97d1 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 21 Jun 2023 21:15:17 +0800 Subject: [PATCH 163/168] Support password and verification for daily limit (#199) --- src/app/components/limit/filter.ts | 13 +- .../components/limit/table/column/common.ts | 68 +++++++ .../components/limit/table/column/delay.ts | 18 +- .../components/limit/table/column/enabled.ts | 18 +- .../limit/table/column/operation.ts | 40 +++- src/app/components/limit/table/index.ts | 1 - .../option/components/appearance/index.ts | 134 +++++-------- .../option/components/backup/footer.ts | 4 +- .../option/components/backup/index.ts | 185 +++++++++--------- .../components/daily-limit/daily-limit.sass | 6 + .../option/components/daily-limit/index.ts | 134 +++++++++++++ src/app/components/option/index.ts | 13 +- src/content-script/locale.ts | 4 +- src/i18n/message/app/index.ts | 144 ++++---------- src/i18n/message/app/limit-resource.json | 28 ++- src/i18n/message/app/limit.ts | 10 + src/i18n/message/app/option-resource.json | 87 +++++--- src/i18n/message/app/option.ts | 20 +- src/i18n/message/common/button.ts | 4 +- src/service/limit-service/index.ts | 137 +++++++++++++ .../limit-service/verification/common.ts | 37 ++++ .../verification/generator/confession.ts | 24 +++ .../verification/generator/index.ts | 19 ++ .../verification/generator/pi.ts | 44 +++++ .../verification/generator/ugly.ts | 26 +++ .../generator/uncommon-chinese.ts | 34 ++++ .../limit-service/verification/processor.ts | 34 ++++ src/service/option-service.ts | 11 ++ src/util/constant/option.ts | 11 +- src/util/number.ts | 7 + types/timer/limit.d.ts | 20 ++ types/timer/option.d.ts | 24 ++- 32 files changed, 1017 insertions(+), 342 deletions(-) create mode 100644 src/app/components/limit/table/column/common.ts create mode 100644 src/app/components/option/components/daily-limit/daily-limit.sass create mode 100644 src/app/components/option/components/daily-limit/index.ts create mode 100644 src/service/limit-service/index.ts create mode 100644 src/service/limit-service/verification/common.ts create mode 100644 src/service/limit-service/verification/generator/confession.ts create mode 100644 src/service/limit-service/verification/generator/index.ts create mode 100644 src/service/limit-service/verification/generator/pi.ts create mode 100644 src/service/limit-service/verification/generator/ugly.ts create mode 100644 src/service/limit-service/verification/generator/uncommon-chinese.ts create mode 100644 src/service/limit-service/verification/processor.ts diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts index 24602faa0..91edb34ee 100644 --- a/src/app/components/limit/filter.ts +++ b/src/app/components/limit/filter.ts @@ -5,17 +5,22 @@ * https://opensource.org/licenses/MIT */ -import { Operation, Plus } from "@element-plus/icons-vue" +import { Operation, Plus, SetUp } from "@element-plus/icons-vue" import { Ref, h, defineComponent, ref } from "vue" import InputFilterItem from "@app/components/common/input-filter-item" import SwitchFilterItem from "@app/components/common/switch-filter-item" import ButtonFilterItem from "@app/components/common/button-filter-item" import { t } from "@app/locale" +import { getAppPageUrl } from "@util/constant/url" +import { OPTION_ROUTE } from "@app/router/constants" +import { createTabAfterCurrent } from "@api/chrome/tab" const urlPlaceholder = t(msg => msg.limit.conditionFilter) const onlyEnabledLabel = t(msg => msg.limit.filterDisabled) const addButtonText = t(msg => msg.button.create) const testButtonText = t(msg => msg.limit.button.test) +const optionButtonText = t(msg => msg.limit.button.option) +const optionPageUrl = getAppPageUrl(false, OPTION_ROUTE, { i: 'dailyLimit' }) const emits = { create: () => true, @@ -60,6 +65,12 @@ const _default = defineComponent({ icon: Operation, onClick: () => ctx.emit('test') }), + h(ButtonFilterItem, { + text: optionButtonText, + icon: SetUp, + type: 'primary', + onClick: () => createTabAfterCurrent(optionPageUrl) + }), h(ButtonFilterItem, { text: addButtonText, type: "success", diff --git a/src/app/components/limit/table/column/common.ts b/src/app/components/limit/table/column/common.ts new file mode 100644 index 000000000..b456c87c2 --- /dev/null +++ b/src/app/components/limit/table/column/common.ts @@ -0,0 +1,68 @@ +import { t, tN } from "@app/locale" +import { locale } from "@i18n" +import { VerificationPair } from "@service/limit-service/verification/common" +import verificationProcessor from "@service/limit-service/verification/processor" +import { ElMessageBox, ElMessage } from "element-plus" +import { h, VNode } from "vue" + +/** + * Judge wether verification is required + * + * @returns T/F + */ +export function judgeVerificationRequired(item: timer.limit.Item): boolean { + const { waste, time } = item || {} + return !!(waste > time * 1000) +} + +const PROMT_TXT_CSS: Partial = { + userSelect: 'none', +} + +/** + * @returns null if verification not required, + * or promise with resolve invocked only if verification code or password correct + */ +export async function processVerification(option: timer.option.DailyLimitOption): Promise { + const { limitLevel, limitPassword, limitVerifyDifficulty } = option + let answerValue: string + let messageNodes: (VNode | string)[] + let incrorectMessage: string + if (limitLevel === 'password' && limitPassword) { + answerValue = limitPassword + messageNodes = [t(msg => msg.limit.verification.pswInputTip)] + incrorectMessage = t(msg => msg.limit.verification.incorrectPsw) + } else if (limitLevel === 'verification') { + const pair: VerificationPair = verificationProcessor.generate(limitVerifyDifficulty, locale) + const { prompt, promptParam, answer } = pair || {} + answerValue = typeof answer === 'function' ? t(msg => answer(msg.limit.verification)) : answer + incrorectMessage = t(msg => msg.limit.verification.incorrectAnswer) + if (prompt) { + const promptTxt = typeof prompt === 'function' + ? t(msg => prompt(msg.limit.verification), { ...promptParam, answer: answerValue }) + : prompt + messageNodes = tN(msg => msg.limit.verification.inputTip, { prompt: h('b', promptTxt) }) + } else { + messageNodes = tN(msg => msg.limit.verification.inputTip2, { answer: h('b', answerValue) }) + } + } + return messageNodes?.length && answerValue + ? new Promise(resolve => + ElMessageBox({ + boxType: 'prompt', + type: 'warning', + title: '', + message: h('div', { style: PROMT_TXT_CSS }, messageNodes), + showInput: true, + showCancelButton: true, + showClose: true, + }).then(data => { + const { value } = data + if (value === answerValue) { + return resolve() + } + ElMessage.error(incrorectMessage) + }) + ) + : null +} diff --git a/src/app/components/limit/table/column/delay.ts b/src/app/components/limit/table/column/delay.ts index 616aa280c..49418b2db 100644 --- a/src/app/components/limit/table/column/delay.ts +++ b/src/app/components/limit/table/column/delay.ts @@ -9,10 +9,24 @@ import { InfoFilled } from "@element-plus/icons-vue" import { ElIcon, ElSwitch, ElTableColumn, ElTooltip } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import { judgeVerificationRequired, processVerification } from "./common" +import optionService from "@service/option-service" const label = t(msg => msg.limit.item.delayAllowed) const tooltip = t(msg => msg.limit.item.delayAllowedInfo) +async function handleChange(row: timer.limit.Item, newVal: boolean, callback: () => void) { + let promise: Promise = null + if (newVal && judgeVerificationRequired(row)) { + // Open delay for limited rules, so verification is required + const option = await optionService.getAllOption() + promise = processVerification(option) + } + promise + ? promise.then(callback).catch(() => { }) + : callback() +} + const _default = defineComponent({ name: "LimitDelayColumn", emits: { @@ -26,10 +40,10 @@ const _default = defineComponent({ }, { default: ({ row }: { row: timer.limit.Item }) => h(ElSwitch, { modelValue: row.allowDelay, - onChange(val: boolean) { + onChange: (val: boolean) => handleChange(row, val, () => { row.allowDelay = val ctx.emit("rowChange", row, val) - } + }) }), header: () => h('div', [ label, diff --git a/src/app/components/limit/table/column/enabled.ts b/src/app/components/limit/table/column/enabled.ts index 60b1d0ac9..6e690fd73 100644 --- a/src/app/components/limit/table/column/enabled.ts +++ b/src/app/components/limit/table/column/enabled.ts @@ -8,9 +8,23 @@ import { ElSwitch, ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import { judgeVerificationRequired, processVerification } from "./common" +import optionService from "@service/option-service" const label = t(msg => msg.limit.item.enabled) +async function handleChange(row: timer.limit.Item, newVal: boolean, callback: () => void) { + let promise: Promise = null + if (!newVal && judgeVerificationRequired(row)) { + // Disable limited rules, so verification is required + const option = await optionService.getAllOption() + promise = processVerification(option) + } + promise + ? promise.then(callback).catch(() => { }) + : callback() +} + const _default = defineComponent({ name: "LimitEnabledColumn", emits: { @@ -25,10 +39,10 @@ const _default = defineComponent({ }, { default: ({ row }: { row: timer.limit.Item }) => h(ElSwitch, { modelValue: row.enabled, - onChange(val: boolean) { + onChange: (val: boolean) => handleChange(row, val, () => { row.enabled = val ctx.emit("rowChange", row, val) - } + }) }) }) } diff --git a/src/app/components/limit/table/column/operation.ts b/src/app/components/limit/table/column/operation.ts index e951c6bd4..d34a97121 100644 --- a/src/app/components/limit/table/column/operation.ts +++ b/src/app/components/limit/table/column/operation.ts @@ -9,12 +9,40 @@ import { Delete, Edit } from "@element-plus/icons-vue" import { ElButton, ElMessageBox, ElTableColumn } from "element-plus" import { defineComponent, h } from "vue" import { t } from "@app/locale" +import optionService from "@service/option-service" +import { judgeVerificationRequired, processVerification } from "./common" const label = t(msg => msg.limit.item.operation) const deleteButtonText = t(msg => msg.button.delete) const modifyButtonText = t(msg => msg.button.modify) + +async function handleDelete(row: timer.limit.Item, callback: () => void) { + let promise = undefined + if (judgeVerificationRequired(row)) { + const option = await optionService.getAllOption() as timer.option.DailyLimitOption + promise = processVerification(option) + } + if (!promise) { + const message = t(msg => msg.limit.message.deleteConfirm, { cond: row.cond }) + promise = ElMessageBox.confirm(message, { type: 'warning' }) + } + promise.then(callback).catch(() => { /** Do nothing */ }) +} + +async function handleModify(row: timer.limit.Item, callback: () => void) { + let promise: Promise = undefined + if (judgeVerificationRequired(row)) { + const option = await optionService.getAllOption() as timer.option.DailyLimitOption + promise = processVerification(option) + promise + ? promise.then(callback).catch(() => { }) + : callback() + } else { + callback() + } +} + const _default = defineComponent({ - name: "LimitOperationColumn", emits: { rowDelete: (_row: timer.limit.Item, _cond: string) => true, rowModify: (_row: timer.limit.Item) => true, @@ -31,19 +59,13 @@ const _default = defineComponent({ type: 'danger', size: 'small', icon: Delete, - onClick() { - const { cond } = row - const message = t(msg => msg.limit.message.deleteConfirm, { cond }) - ElMessageBox.confirm(message, { type: 'warning' }) - .then(() => ctx.emit("rowDelete", row, cond)) - .catch(() => { /** Do nothing */ }) - } + onClick: () => handleDelete(row, () => ctx.emit("rowDelete", row, row.cond)) }, () => deleteButtonText), h(ElButton, { type: 'primary', size: 'small', icon: Edit, - onClick: () => ctx.emit('rowModify', row), + onClick: () => handleModify(row, () => ctx.emit('rowModify', row)), }, () => modifyButtonText) ] }) diff --git a/src/app/components/limit/table/index.ts b/src/app/components/limit/table/index.ts index 07f4bd181..ff55803a1 100644 --- a/src/app/components/limit/table/index.ts +++ b/src/app/components/limit/table/index.ts @@ -15,7 +15,6 @@ import LimitEnabledColumn from "./column/enabled" import LimitOperationColumn from "./column/operation" const _default = defineComponent({ - name: "LimitTable", props: { data: Array as PropType }, diff --git a/src/app/components/option/components/appearance/index.ts b/src/app/components/option/components/appearance/index.ts index 6096dabe6..a9b468116 100644 --- a/src/app/components/option/components/appearance/index.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -80,24 +80,6 @@ const locale = (option: UnwrapRef) => h(ElSelect, ) }) -const ALL_LIMIT_FILTER_TYPE: timer.limit.FilterType[] = [ - 'translucent', - 'groundGlass', -] - -const limitFilterTypeSelect = (option: timer.option.AppearanceOption) => h(ElSelect, { - modelValue: option.limitMarkFilter, - size: 'small', - onChange: (val: timer.limit.FilterType) => { - option.limitMarkFilter = val - optionService.setAppearanceOption(unref(option)) - } -}, { - default: () => ALL_LIMIT_FILTER_TYPE.map(item => - h(ElOption, { value: item, label: t(msg => msg.option.appearance.limitFilterType[item]) }) - ) -}) - function copy(target: timer.option.AppearanceOption, source: timer.option.AppearanceOption) { target.displayWhitelistMenu = source.displayWhitelistMenu target.displayBadgeText = source.displayBadgeText @@ -109,69 +91,59 @@ function copy(target: timer.option.AppearanceOption, source: timer.option.Appear target.limitMarkFilter = source.limitMarkFilter } -const _default = defineComponent({ - name: "AppearanceOptionContainer", - setup(_props, ctx) { - const option: UnwrapRef = reactive(defaultAppearance()) - optionService.getAllOption().then(currentVal => copy(option, currentVal)) - ctx.expose({ - async reset() { - copy(option, defaultAppearance()) - await optionService.setAppearanceOption(unref(option)) - toggle(await optionService.isDarkMode(option)) - } - }) - return () => h('div', [ - renderOptionItem({ - input: h(DarkModeInput, { - modelValue: option.darkMode, - startSecond: option.darkModeTimeStart, - endSecond: option.darkModeTimeEnd, - onChange: async (darkMode, range) => { - option.darkMode = darkMode - option.darkModeTimeStart = range?.[0] - option.darkModeTimeEnd = range?.[1] - await optionService.setAppearanceOption(unref(option)) - toggle(await optionService.isDarkMode()) - } - }) - }, - msg => msg.appearance.darkMode.label, - t(msg => msg.option.appearance.darkMode.options.default)), - h(ElDivider), - renderOptionItem({ - input: locale(option) - }, - msg => msg.appearance.locale.label, - t(msg => msg.option.appearance.locale.default) - ), - h(ElDivider), - renderOptionItem({ - input: displayWhitelist(option), - whitelist: tagText(msg => msg.option.appearance.whitelistItem), - contextMenu: tagText(msg => msg.option.appearance.contextMenu) - }, msg => msg.appearance.displayWhitelist, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: displayBadgeText(option), - timeInfo: tagText(msg => msg.option.appearance.badgeTextContent), - icon: tagText(msg => msg.option.appearance.icon) - }, msg => msg.appearance.displayBadgeText, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: printInConsole(option), - console: tagText(msg => msg.option.appearance.printInConsole.console), - info: tagText(msg => msg.option.appearance.printInConsole.info) - }, msg => msg.appearance.printInConsole.label, t(msg => msg.option.yes)), - h(ElDivider), - renderOptionItem({ - input: limitFilterTypeSelect(option) - }, - msg => msg.appearance.limitFilterType.label, - t(msg => msg.option.appearance.limitFilterType[defaultAppearance().limitMarkFilter]) - ) - ]) - } +const _default = defineComponent((_props, ctx) => { + const option: UnwrapRef = reactive(defaultAppearance()) + optionService.getAllOption().then(currentVal => copy(option, currentVal)) + ctx.expose({ + async reset() { + copy(option, defaultAppearance()) + await optionService.setAppearanceOption(unref(option)) + toggle(await optionService.isDarkMode(option)) + } + }) + return () => h('div', [ + renderOptionItem({ + input: h(DarkModeInput, { + modelValue: option.darkMode, + startSecond: option.darkModeTimeStart, + endSecond: option.darkModeTimeEnd, + onChange: async (darkMode, range) => { + option.darkMode = darkMode + option.darkModeTimeStart = range?.[0] + option.darkModeTimeEnd = range?.[1] + await optionService.setAppearanceOption(unref(option)) + toggle(await optionService.isDarkMode()) + } + }) + }, + msg => msg.appearance.darkMode.label, + t(msg => msg.option.appearance.darkMode.options.default)), + h(ElDivider), + renderOptionItem({ + input: locale(option) + }, + msg => msg.appearance.locale.label, + t(msg => msg.option.appearance.locale.default) + ), + h(ElDivider), + renderOptionItem({ + input: displayWhitelist(option), + whitelist: tagText(msg => msg.option.appearance.whitelistItem), + contextMenu: tagText(msg => msg.option.appearance.contextMenu) + }, msg => msg.appearance.displayWhitelist, t(msg => msg.option.yes)), + h(ElDivider), + renderOptionItem({ + input: displayBadgeText(option), + timeInfo: tagText(msg => msg.option.appearance.badgeTextContent), + icon: tagText(msg => msg.option.appearance.icon) + }, msg => msg.appearance.displayBadgeText, t(msg => msg.option.yes)), + h(ElDivider), + renderOptionItem({ + input: printInConsole(option), + console: tagText(msg => msg.option.appearance.printInConsole.console), + info: tagText(msg => msg.option.appearance.printInConsole.info) + }, msg => msg.appearance.printInConsole.label, t(msg => msg.option.yes)), + ]) }) export default _default \ No newline at end of file diff --git a/src/app/components/option/components/backup/footer.ts b/src/app/components/option/components/backup/footer.ts index 24b300a10..fee152c5b 100644 --- a/src/app/components/option/components/backup/footer.ts +++ b/src/app/components/option/components/backup/footer.ts @@ -4,12 +4,12 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { PropType, Ref, watch } from "vue" +import type { PropType, Ref } from "vue" import { t } from "@app/locale" import { UploadFilled } from "@element-plus/icons-vue" import { ElButton, ElLoading, ElMessage, ElText } from "element-plus" -import { defineComponent, h, ref } from "vue" +import { defineComponent, h, ref, watch } from "vue" import metaService from "@service/meta-service" import processor from "@src/common/backup/processor" import { formatTime } from "@util/time" diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index bb64c1fcd..ec16f0c65 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -81,106 +81,103 @@ const authInput = (auth: Ref, handleInput: Function, handleTest: Functio const DEFAULT = defaultBackup() -const _default = defineComponent({ - name: "BackupOptionContainer", - setup(_props, ctx) { - const type: Ref = ref(DEFAULT.backupType) - const auth: Ref = ref('') - const clientName: Ref = ref(DEFAULT.clientName) - const autoBackUp: Ref = ref(DEFAULT.autoBackUp) - const autoBackUpInterval: Ref = ref(DEFAULT.autoBackUpInterval) - - optionService.getAllOption().then(currentVal => { - clientName.value = currentVal.clientName - type.value = currentVal.backupType - if (type.value) { - auth.value = currentVal.backupAuths?.[type.value] - } - autoBackUp.value = currentVal.autoBackUp - autoBackUpInterval.value = currentVal.autoBackUpInterval - }) - - function handleChange() { - const backupAuths = {} - backupAuths[type.value] = auth.value - const newOption: timer.option.BackupOption = { - backupType: type.value, - backupAuths, - clientName: clientName.value || DEFAULT.clientName, - autoBackUp: autoBackUp.value, - autoBackUpInterval: autoBackUpInterval.value, - } - optionService.setBackupOption(newOption) +const _default = defineComponent((_props, ctx) => { + const type: Ref = ref(DEFAULT.backupType) + const auth: Ref = ref('') + const clientName: Ref = ref(DEFAULT.clientName) + const autoBackUp: Ref = ref(DEFAULT.autoBackUp) + const autoBackUpInterval: Ref = ref(DEFAULT.autoBackUpInterval) + + optionService.getAllOption().then(currentVal => { + clientName.value = currentVal.clientName + type.value = currentVal.backupType + if (type.value) { + auth.value = currentVal.backupAuths?.[type.value] } - - async function handleTest() { - const loading = ElLoading.service({ - text: "Please wait...." - }) - const errorMsg = await processor.test(type.value, auth.value) - loading.close() - if (!errorMsg) { - ElMessage.success("Valid!") - } else { - ElMessage.error(errorMsg) - } + autoBackUp.value = currentVal.autoBackUp + autoBackUpInterval.value = currentVal.autoBackUpInterval + }) + + function handleChange() { + const backupAuths = {} + backupAuths[type.value] = auth.value + const newOption: timer.option.BackupOption = { + backupType: type.value, + backupAuths, + clientName: clientName.value || DEFAULT.clientName, + autoBackUp: autoBackUp.value, + autoBackUpInterval: autoBackUpInterval.value, } + optionService.setBackupOption(newOption) + } - ctx.expose({ - async reset() { - // Only reset type and auto flag - type.value = DEFAULT.backupType - autoBackUp.value = DEFAULT.autoBackUp - handleChange() - } + async function handleTest() { + const loading = ElLoading.service({ + text: "Please wait...." }) + const errorMsg = await processor.test(type.value, auth.value) + loading.close() + if (!errorMsg) { + ElMessage.success("Valid!") + } else { + ElMessage.error(errorMsg) + } + } - return () => { - const nodes = [ - h(ElAlert, { - closable: false, - type: "warning", - description: t(msg => msg.option.backup.alert, { email: AUTHOR_EMAIL }) - }), - h(ElDivider), - renderOptionItem({ - input: typeSelect(type, handleChange) - }, - msg => msg.backup.type, - t(TYPE_NAMES[DEFAULT.backupType]) - ) - ] - type.value !== 'none' && nodes.push( - h(ElDivider), - renderOptionItem({ - input: h(BackUpAutoInput, { - autoBackup: autoBackUp.value, - interval: autoBackUpInterval.value, - onChange(newAutoBackUp, newInterval) { - autoBackUp.value = newAutoBackUp - autoBackUpInterval.value = newInterval - handleChange() - } - }) - }, _msg => '{input}', t(msg => msg.option.no)), - h(ElDivider), - renderOptionItem({ - input: authInput(auth, handleChange, handleTest), - info: tooltip(msg => msg.option.backup.meta[type.value]?.authInfo) - }, - _msg => AUTH_LABELS[type.value], - ), - h(ElDivider), - renderOptionItem({ - input: clientNameInput(clientName, handleChange) - }, - msg => msg.backup.client - ), - h(ElDivider), - h(Footer, { type: type.value }), - ) - return h('div', nodes) + ctx.expose({ + async reset() { + // Only reset type and auto flag + type.value = DEFAULT.backupType + autoBackUp.value = DEFAULT.autoBackUp + handleChange() } + }) + + return () => { + const nodes = [ + h(ElAlert, { + closable: false, + type: "warning", + description: t(msg => msg.option.backup.alert, { email: AUTHOR_EMAIL }) + }), + h(ElDivider), + renderOptionItem({ + input: typeSelect(type, handleChange) + }, + msg => msg.backup.type, + t(TYPE_NAMES[DEFAULT.backupType]) + ) + ] + type.value !== 'none' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: h(BackUpAutoInput, { + autoBackup: autoBackUp.value, + interval: autoBackUpInterval.value, + onChange(newAutoBackUp, newInterval) { + autoBackUp.value = newAutoBackUp + autoBackUpInterval.value = newInterval + handleChange() + } + }) + }, _msg => '{input}', t(msg => msg.option.no)), + h(ElDivider), + renderOptionItem({ + input: authInput(auth, handleChange, handleTest), + info: tooltip(msg => msg.option.backup.meta[type.value]?.authInfo) + }, + _msg => AUTH_LABELS[type.value], + ), + h(ElDivider), + renderOptionItem({ + input: clientNameInput(clientName, handleChange) + }, + msg => msg.backup.client + ), + h(ElDivider), + h(Footer, { type: type.value }), + ) + return h('div', nodes) } }) diff --git a/src/app/components/option/components/daily-limit/daily-limit.sass b/src/app/components/option/components/daily-limit/daily-limit.sass new file mode 100644 index 000000000..f31c2c536 --- /dev/null +++ b/src/app/components/option/components/daily-limit/daily-limit.sass @@ -0,0 +1,6 @@ +// Fallback with EN +.option-daily-limit-level-select>.select-trigger + width: 330px + +.option-daily-limit-level-select.zh_CN>.select-trigger + width: 210px diff --git a/src/app/components/option/components/daily-limit/index.ts b/src/app/components/option/components/daily-limit/index.ts new file mode 100644 index 000000000..a5448f2d1 --- /dev/null +++ b/src/app/components/option/components/daily-limit/index.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@app/locale" +import { locale } from "@i18n" +import optionService from "@service/option-service" +import { defaultDailyLimit } from "@util/constant/option" +import { ElDivider, ElInput, ElOption, ElSelect } from "element-plus" +import { defineComponent, reactive, unref, UnwrapRef, h } from "vue" +import { renderOptionItem } from "../../common" +import "./daily-limit.sass" + +const ALL_LIMIT_FILTER_TYPE: timer.limit.FilterType[] = [ + 'translucent', + 'groundGlass', +] + +const ALL_LEVEL: timer.limit.RestrictionLevel[] = [ + 'nothing', + 'verification', + 'password', +] + +const ALL_DIFF: timer.limit.VerificationDifficulty[] = [ + 'easy', + 'hard', + 'disgusting', +] + +const filterSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitFilter, + size: 'small', + onChange: (val: timer.limit.FilterType) => { + option.limitFilter = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_LIMIT_FILTER_TYPE.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.filter[item]) }))) + +const levelSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitLevel, + size: 'small', + class: `option-daily-limit-level-select ${locale}`, + onChange: (val: timer.limit.RestrictionLevel) => { + option.limitLevel = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_LEVEL.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.level[item]) }))) + +const pswInput = (option: timer.option.DailyLimitOption) => h(ElInput, { + modelValue: option.limitPassword, + size: 'small', + type: 'password', + showPassword: true, + style: { width: '200px' }, + onInput: (val: string) => { + option.limitPassword = val?.trim() + optionService.setDailyLimitOption(unref(option)) + } +}) + +const veriDiffSelect = (option: timer.option.DailyLimitOption) => h(ElSelect, { + modelValue: option.limitVerifyDifficulty, + size: 'small', + onChange: (val: timer.limit.VerificationDifficulty) => { + option.limitVerifyDifficulty = val + optionService.setDailyLimitOption(unref(option)) + } +}, () => ALL_DIFF.map(item => h(ElOption, { value: item, label: t(msg => msg.option.dailyLimit.level.verificationDifficulty[item]) }))) + +function copy(target: timer.option.DailyLimitOption, source: timer.option.DailyLimitOption) { + target.limitFilter = source.limitFilter + target.limitLevel = source.limitLevel + target.limitPassword = source.limitPassword + target.limitVerifyDifficulty = source.limitVerifyDifficulty +} + +function reset(target: timer.option.DailyLimitOption) { + const defaultValue = defaultDailyLimit() + // Not to reset limitPassword + delete defaultValue.limitPassword + // Not to reset difficulty + delete defaultValue.limitVerifyDifficulty + Object.entries(defaultValue).forEach(([key, val]) => target[key] = val) +} + +const _default = defineComponent((_, ctx) => { + const option: UnwrapRef = reactive(defaultDailyLimit()) + optionService.getAllOption().then(currentVal => { + copy(option, currentVal) + }) + ctx.expose({ + reset: () => reset(option) + }) + return () => { + const nodes = [ + renderOptionItem({ + input: filterSelect(option) + }, + msg => msg.dailyLimit.filter.label, + t(msg => msg.option.dailyLimit.filter[defaultDailyLimit().limitFilter]) + ), + h(ElDivider), + renderOptionItem({ + input: levelSelect(option) + }, + msg => msg.dailyLimit.level.label, + t(msg => msg.option.dailyLimit.level[defaultDailyLimit().limitLevel]) + ), + ] + const { limitLevel } = option + limitLevel === 'password' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: pswInput(option), + }, msg => msg.dailyLimit.level.passwordLabel) + ) + limitLevel === 'verification' && nodes.push( + h(ElDivider), + renderOptionItem({ + input: veriDiffSelect(option), + }, + msg => msg.dailyLimit.level.verificationLabel, + t(msg => msg.option.dailyLimit.level[defaultDailyLimit().limitVerifyDifficulty]) + ) + ) + return nodes + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/index.ts b/src/app/components/option/index.ts index 675326ae7..207ec1089 100644 --- a/src/app/components/option/index.ts +++ b/src/app/components/option/index.ts @@ -14,6 +14,7 @@ import Popup from "./components/popup" import Appearance from "./components/appearance" import Statistics from "./components/statistics" import Backup from './components/backup' +import DailyLimit from './components/daily-limit' import './style' import { ElIcon, ElMessage, ElTabPane, ElTabs } from "element-plus" import { t } from "@app/locale" @@ -22,7 +23,7 @@ import { useRoute, useRouter } from "vue-router" const resetButtonName = "reset" const initialParamName = "i" -const allCategories = ["appearance", "statistics", "popup", 'backup'] as const +const allCategories = ["appearance", "statistics", "popup", 'dailyLimit', 'backup'] as const type _Category = typeof allCategories[number] function initWithQuery(tab: Ref<_Category>) { @@ -78,6 +79,7 @@ const _default = defineComponent({ statistics: ref(), popup: ref(), backup: ref(), + dailyLimit: ref(), } const router = useRouter() return () => h(ContentContainer, () => h(ElTabs, { @@ -106,7 +108,14 @@ const _default = defineComponent({ }, () => h(Popup, { ref: paneRefMap.popup })), - // backup + // Limit + h(ElTabPane, { + label: t(msg => msg.menu.limit), + name: "dailyLimit" as _Category + }, () => h(DailyLimit, { + ref: paneRefMap.dailyLimit + })), + // Backup h(ElTabPane, { label: t(msg => msg.option.backup.title), name: "backup" as _Category diff --git a/src/content-script/locale.ts b/src/content-script/locale.ts index 2aa3a5302..8771f4da9 100644 --- a/src/content-script/locale.ts +++ b/src/content-script/locale.ts @@ -10,6 +10,6 @@ import type { ContentScriptMessage } from "@i18n/message/common/content-script" import { t as t_ } from "@i18n" import messages from "@i18n/message/common/content-script" -export function t(key: I18nKey): string { - return t_(messages, { key }) +export function t(key: I18nKey, param?: any): string { + return t_(messages, { key, param }) } \ No newline at end of file diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 086113dc6..2aaae5051 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -49,117 +49,41 @@ export type AppMessage = { button: ButtonMessage } +const CHILD_MESSAGES: { [key in keyof AppMessage]: Messages } = { + dataManage: dataManageMessages, + item: itemMessages, + mergeCommon: mergeCommonMessages, + report: reportMessages, + whitelist: whitelistMessages, + mergeRule: mergeRuleMessages, + option: optionMessages, + analysis: analysisMessages, + menu: menuMessages, + habit: habitMessages, + limit: limitMessages, + siteManage: siteManageManages, + operation: operationMessages, + confirm: confirmMessages, + dashboard: dashboardMessages, + calendar: calendarMessages, + timeFormat: timeFormatMessages, + duration: popupDurationMessages, + helpUs: helpUsMessages, + button: buttonMessages, +} + +function appMessageOf(locale: timer.Locale): AppMessage { + const entries: [string, any][] = Object.entries(CHILD_MESSAGES).map(([key, val]) => ([key, val[locale]])) + const result = Object.fromEntries(entries) as AppMessage + return result +} + const _default: Messages = { - zh_CN: { - dataManage: dataManageMessages.zh_CN, - item: itemMessages.zh_CN, - mergeCommon: mergeCommonMessages.zh_CN, - report: reportMessages.zh_CN, - whitelist: whitelistMessages.zh_CN, - mergeRule: mergeRuleMessages.zh_CN, - option: optionMessages.zh_CN, - analysis: analysisMessages.zh_CN, - menu: menuMessages.zh_CN, - habit: habitMessages.zh_CN, - limit: limitMessages.zh_CN, - siteManage: siteManageManages.zh_CN, - operation: operationMessages.zh_CN, - confirm: confirmMessages.zh_CN, - dashboard: dashboardMessages.zh_CN, - calendar: calendarMessages.zh_CN, - timeFormat: timeFormatMessages.zh_CN, - duration: popupDurationMessages.zh_CN, - helpUs: helpUsMessages.zh_CN, - button: buttonMessages.zh_CN, - }, - zh_TW: { - dataManage: dataManageMessages.zh_TW, - item: itemMessages.zh_TW, - mergeCommon: mergeCommonMessages.zh_TW, - report: reportMessages.zh_TW, - whitelist: whitelistMessages.zh_TW, - mergeRule: mergeRuleMessages.zh_TW, - option: optionMessages.zh_TW, - analysis: analysisMessages.zh_TW, - menu: menuMessages.zh_TW, - habit: habitMessages.zh_TW, - limit: limitMessages.zh_TW, - siteManage: siteManageManages.zh_TW, - operation: operationMessages.zh_TW, - confirm: confirmMessages.zh_TW, - dashboard: dashboardMessages.zh_TW, - calendar: calendarMessages.zh_TW, - timeFormat: timeFormatMessages.zh_TW, - duration: popupDurationMessages.zh_TW, - helpUs: helpUsMessages.zh_TW, - button: buttonMessages.zh_TW, - }, - en: { - dataManage: dataManageMessages.en, - item: itemMessages.en, - mergeCommon: mergeCommonMessages.en, - report: reportMessages.en, - whitelist: whitelistMessages.en, - mergeRule: mergeRuleMessages.en, - option: optionMessages.en, - analysis: analysisMessages.en, - menu: menuMessages.en, - habit: habitMessages.en, - limit: limitMessages.en, - siteManage: siteManageManages.en, - operation: operationMessages.en, - confirm: confirmMessages.en, - dashboard: dashboardMessages.en, - calendar: calendarMessages.en, - timeFormat: timeFormatMessages.en, - duration: popupDurationMessages.en, - helpUs: helpUsMessages.en, - button: buttonMessages.en, - }, - ja: { - dataManage: dataManageMessages.ja, - item: itemMessages.ja, - mergeCommon: mergeCommonMessages.ja, - report: reportMessages.ja, - whitelist: whitelistMessages.ja, - mergeRule: mergeRuleMessages.ja, - option: optionMessages.ja, - analysis: analysisMessages.ja, - menu: menuMessages.ja, - habit: habitMessages.ja, - limit: limitMessages.ja, - siteManage: siteManageManages.ja, - operation: operationMessages.ja, - confirm: confirmMessages.ja, - dashboard: dashboardMessages.ja, - calendar: calendarMessages.ja, - timeFormat: timeFormatMessages.ja, - duration: popupDurationMessages.ja, - helpUs: helpUsMessages.ja, - button: buttonMessages.ja, - }, - pt_PT: { - dataManage: dataManageMessages.pt_PT, - item: itemMessages.pt_PT, - mergeCommon: mergeCommonMessages.pt_PT, - report: reportMessages.pt_PT, - whitelist: whitelistMessages.pt_PT, - mergeRule: mergeRuleMessages.pt_PT, - option: optionMessages.pt_PT, - analysis: analysisMessages.pt_PT, - menu: menuMessages.pt_PT, - habit: habitMessages.pt_PT, - limit: limitMessages.pt_PT, - siteManage: siteManageManages.pt_PT, - operation: operationMessages.pt_PT, - confirm: confirmMessages.pt_PT, - dashboard: dashboardMessages.pt_PT, - calendar: calendarMessages.pt_PT, - timeFormat: timeFormatMessages.pt_PT, - duration: popupDurationMessages.pt_PT, - helpUs: helpUsMessages.pt_PT, - button: buttonMessages.pt_PT, - }, + zh_CN: appMessageOf('zh_CN'), + zh_TW: appMessageOf('zh_TW'), + en: appMessageOf('en'), + ja: appMessageOf('ja'), + pt_PT: appMessageOf('pt_PT'), } export default _default \ No newline at end of file diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 4af1ed5bd..ad3ae6825 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -12,7 +12,8 @@ "operation": "操作" }, "button": { - "test": "网址测试" + "test": "网址测试", + "option": "全局设置" }, "addTitle": "新增限制", "useWildcard": "是否使用通配符", @@ -28,7 +29,16 @@ "noRuleMatched": "该网址未命中任何规则", "rulesMatched": "该网址命中以下规则:" }, - "urlPlaceholder": "请直接粘贴网址 ➡️" + "urlPlaceholder": "请直接粘贴网址 ➡️", + "verification": { + "inputTip": "该规则已超时,如要表更,请输入以下问题的答案:{prompt}", + "inputTip2": "该规则已超时,如要表更,请在下列输入框中原样输入:{answer}", + "pswInputTip": "该规则已超时,如要表更,所以请在下列输入框中输入您的解锁密码", + "incorrectPsw": "密码错误", + "incorrectAnswer": "回答错误", + "pi": "圆周率 π 的小数部分第 {startIndex} 位到第 {endIndex} 位的共 {digitCount} 位数字", + "confession": "一寸光一寸金,寸金难买寸光阴" + } }, "zh_TW": { "conditionFilter": "輸入網址,然後回車", @@ -74,7 +84,8 @@ "operation": "Operations" }, "button": { - "test": "Test URL" + "test": "Test URL", + "option": "Options" }, "addTitle": "New", "useWildcard": "Whether to use wildcard", @@ -90,7 +101,16 @@ "noRuleMatched": "The URL does not hit any rules", "rulesMatched": "The URL hits the following rules:" }, - "urlPlaceholder": "Please paste the URL directly ➡️" + "urlPlaceholder": "Please paste the URL directly ➡️", + "verification": { + "inputTip": "This rule has already been triggered. To modify it, please enter the answer to the following prompt: {prompt}", + "inputTip2": "This rule has already been triggered. To modify it, please enter it as it is: {answer}", + "pswInputTip": "This rule has already been triggered. To modify it, please enter your unlock password", + "incorrectPsw": "Incorrect password", + "incorrectAnswer": "Incorrect answer", + "pi": "{digitCount} digits from {startIndex} to {endIndex} of the decimal part of π", + "confession": "Time is fleeting" + } }, "ja": { "conditionFilter": "URL", diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index fab37bdf3..fb165e0ab 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -24,6 +24,7 @@ export type LimitMessage = { } button: { test: string + option: string } message: { noUrl: string @@ -37,6 +38,15 @@ export type LimitMessage = { noRuleMatched: string rulesMatched: string } + verification: { + inputTip: string + inputTip2: string + pswInputTip: string + incorrectPsw: string + incorrectAnswer: string + pi: string + confession: string + } } const _default: Messages = resource diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index 036125a4c..a6bf678b6 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -39,11 +39,6 @@ "off": "始终关闭", "timed": "定时开启" } - }, - "limitFilterType": { - "label": "每日时限的背景风格 {input}", - "translucent": "半透明", - "groundGlass": "毛玻璃" } }, "statistics": { @@ -58,6 +53,26 @@ "siteName": "网站的名称", "siteNameUsage": "数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称" }, + "dailyLimit": { + "filter": { + "label": "模态页面的背景风格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + }, + "level": { + "label": "受限时如何解锁 {input}", + "nothing": "允许在管理页面直接解锁", + "password": "必须输入密码解锁", + "verification": "必须输入验证码解锁", + "passwordLabel": "解锁密码 {input}", + "verificationLabel": "验证码的难度 {input}", + "verificationDifficulty": { + "easy": "简单", + "hard": "困难", + "disgusting": "折磨" + } + } + }, "backup": { "title": "数据备份", "type": "远端类型 {input}", @@ -123,11 +138,6 @@ "off": "始終關閉", "timed": "定時開啟" } - }, - "limitFilterType": { - "label": "每日時限的背景風格 {input}", - "translucent": "半透明", - "groundGlass": "毛玻璃" } }, "statistics": { @@ -142,6 +152,13 @@ "siteName": "網站的名稱", "siteNameUsage": "數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱" }, + "dailyLimit": { + "filter": { + "label": "模態頁面的背景風格 {input}", + "translucent": "半透明", + "groundGlass": "毛玻璃" + } + }, "backup": { "title": "數據備份", "type": "雲端類型 {input}", @@ -207,11 +224,6 @@ "off": "Always off", "timed": "Timed on" } - }, - "limitFilterType": { - "label": "Background style for daily time limit {input}", - "translucent": "Translucent", - "groundGlass": "Ground Glass" } }, "statistics": { @@ -226,6 +238,27 @@ "siteName": "the site name", "siteNameUsage": "The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site." }, + "dailyLimit": { + "title": "Daily Limit", + "filter": { + "label": "The background style of the modal page {input}", + "translucent": "Translucent", + "groundGlass": "Ground Glass" + }, + "level": { + "label": "How to unlock while restricted {input}", + "nothing": "Allow direct unlocking on the admin page", + "password": "Must enter password to unlock", + "verification": "Must enter verification code to unlock", + "passwordLabel": "Password to unlock {input}", + "verificationLabel": "The difficulty of verification code {input}", + "verificationDifficulty": { + "easy": "Easy", + "hard": "Hard", + "disgusting": "Disgusting" + } + } + }, "backup": { "title": "Data Backup", "type": "Remote type {input}", @@ -291,11 +324,6 @@ "off": "常にオフ", "timed": "時限スタート" } - }, - "limitFilterType": { - "label": "毎日の時間制限の背景スタイル {input}", - "translucent": "半透明", - "groundGlass": "すりガラス" } }, "statistics": { @@ -310,6 +338,13 @@ "siteName": "サイト名", "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。" }, + "dailyLimit": { + "filter": { + "label": "モーダルページの背景スタイル {input}", + "translucent": "半透明", + "groundGlass": "すりガラス" + } + }, "backup": { "title": "データバックアップ", "type": "バックアップ方法 {input}", @@ -377,11 +412,6 @@ "off": "Sempre desativado", "timed": "Cronometrando" } - }, - "limitFilterType": { - "label": "Estilo de fundo para limite de tempo diário {input}", - "translucent": "Translúcido", - "groundGlass": "Vidro no Solo" } }, "statistics": { @@ -396,6 +426,13 @@ "siteName": "o nome do site", "siteNameUsage": "Os dados são armazenados apenas localmente e serão exibidos em vez da URL para aumentar o reconhecimento. F, também pode personalizar o nome de cada site." }, + "dailyLimit": { + "filter": { + "label": "O estilo de fundo da página modal {input}", + "translucent": "Translúcido", + "groundGlass": "Vidro no Solo" + } + }, "backup": { "title": "Backup de Dados", "type": "Tipo remoto {input}", diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 05c370c6c..871ad717a 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -45,9 +45,6 @@ export type OptionMessage = { label: string options: Record } - limitFilterType: Record & { - label: string - } } statistics: { title: string @@ -61,6 +58,23 @@ export type OptionMessage = { siteNameUsage: string siteName: string } + dailyLimit: { + filter: { + [filterType in timer.limit.FilterType]: string + } & { + label: string + } + level: { + [level in timer.limit.RestrictionLevel]: string + } & { + label: string + passwordLabel: string + verificationLabel: string + verificationDifficulty: { + [diff in timer.limit.VerificationDifficulty]: string + } + } + } backup: { title: string type: string diff --git a/src/i18n/message/common/button.ts b/src/i18n/message/common/button.ts index caea7af10..0ff343dea 100644 --- a/src/i18n/message/common/button.ts +++ b/src/i18n/message/common/button.ts @@ -20,4 +20,6 @@ export type ButtonMessage = { dont: string } -export default resource as Messages \ No newline at end of file +const _default: Messages = resource + +export default _default \ No newline at end of file diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts new file mode 100644 index 000000000..af0312629 --- /dev/null +++ b/src/service/limit-service/index.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * 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" +import { formatTime } from "@util/time" +import whitelistHolder from '../components/whitelist-holder' + +const storage = chrome.storage.local +const db: LimitDatabase = new LimitDatabase(storage) + +export type QueryParam = { + filterDisabled: boolean + url: string +} + +async function select(cond?: QueryParam): Promise { + const { filterDisabled, url } = cond ? cond : { filterDisabled: undefined, url: undefined } + const today = formatTime(new Date(), DATE_FORMAT) + return (await db.all()) + .filter(item => filterDisabled ? item.enabled : true) + .map(({ cond, time, enabled, wasteTime, latestDate, allowDelay }) => TimeLimitItem.builder() + .cond(cond) + .time(time) + .enabled(enabled) + .waste(latestDate === today ? wasteTime : 0) + .allowDelay(allowDelay) + .build() + ) + // If use url, then test it + .filter(item => url ? item.matches(url) : true) +} + +/** + * Fired if the item is removed or disabled + * + * @param item + */ +async function handleLimitChanged() { + const allItems: TimeLimitItem[] = await select({ filterDisabled: false, url: undefined }) + const tabs = await listTabs() + tabs.forEach(tab => { + const limitedItems = allItems.filter(item => item.matches(tab.url) && item.enabled && item.hasLimited()) + sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) + .catch(err => console.log(err.message)) + }) +} + +async function updateEnabled(item: timer.limit.Item): Promise { + const { cond, time, enabled, allowDelay } = item + const limit: timer.limit.Rule = { cond, time, enabled, allowDelay } + await db.save(limit, true) + await handleLimitChanged() +} + +async function updateDelay(item: timer.limit.Item) { + await db.updateDelay(item.cond, item.allowDelay) + await handleLimitChanged() +} + +async function remove(item: timer.limit.Item): Promise { + await db.remove(item.cond) + await handleLimitChanged() +} + +async function getLimited(url: string): Promise { + const list: TimeLimitItem[] = (await select()) + .filter(item => item.enabled) + .filter(item => item.matches(url)) + .filter(item => item.hasLimited()) + return list +} + +/** + * Add time + * @param url url + * @param focusTime time, milliseconds + * @returns the rules is limit cause of this operation + */ +async function addFocusTime(url: string, focusTime: number) { + const allEnabled: TimeLimitItem[] = await select({ filterDisabled: true, url }) + const toUpdate: { [cond: string]: number } = {} + const result: TimeLimitItem[] = [] + allEnabled.forEach(item => { + const limitBefore = item.hasLimited() + toUpdate[item.cond] = item.waste += focusTime + const limitAfter = item.hasLimited() + if (!limitBefore && limitAfter) { + result.push(item) + } + }) + await db.updateWaste(formatTime(new Date, DATE_FORMAT), toUpdate) + return result +} + +async function moreMinutes(url: string, rules?: TimeLimitItem[]): Promise { + if (rules === undefined || rules === null) { + rules = (await select({ url: url, filterDisabled: true })) + .filter(item => item.hasLimited() && item.allowDelay) + } + const date = formatTime(new Date(), DATE_FORMAT) + const toUpdate: { [cond: string]: number } = {} + rules.forEach(rule => { + const { cond, waste } = rule + const updatedWaste = (waste || 0) - 5 * 60 * 1000 + rule.waste = toUpdate[cond] = updatedWaste < 0 ? 0 : updatedWaste + }) + await db.updateWaste(date, toUpdate) + return rules +} + +class LimitService { + moreMinutes = moreMinutes + getLimited = getLimited + updateEnabled = updateEnabled + updateDelay = updateDelay + select = select + remove = remove + /** + * @returns The rules limited cause of this operation + */ + async addFocusTime(host: string, url: string, focusTime: number): Promise { + if (whitelistHolder.notContains(host)) { + return addFocusTime(url, focusTime) + } else { + return [] + } + } +} + +export default new LimitService() diff --git a/src/service/limit-service/verification/common.ts b/src/service/limit-service/verification/common.ts new file mode 100644 index 000000000..fbc8d02c2 --- /dev/null +++ b/src/service/limit-service/verification/common.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { I18nKey } from "@i18n" +import { LimitMessage } from "@i18n/message/app/limit" + +type LimitVerificationMessage = LimitMessage['verification'] + +export type VerificationContext = { + difficulty: timer.limit.VerificationDifficulty + locale: timer.Locale +} + +export type VerificationPair = { + prompt?: I18nKey | string + promptParam?: any + answer: I18nKey | string +} + +/** + * Verification code generator + */ +export interface VerificationGenerator { + /** + * Whether to support + */ + supports(context: VerificationContext): boolean + + /** + * Render the prompt + */ + generate(context: VerificationContext): VerificationPair +} \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/confession.ts b/src/service/limit-service/verification/generator/confession.ts new file mode 100644 index 000000000..be4414a91 --- /dev/null +++ b/src/service/limit-service/verification/generator/confession.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +/** + * Generator of confession + */ +class ConfessionGenerator implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'easy' + } + generate(_: VerificationContext): VerificationPair { + return { + answer: msg => msg.confession, + } + } +} + +export default ConfessionGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/index.ts b/src/service/limit-service/verification/generator/index.ts new file mode 100644 index 000000000..49f2ed8ec --- /dev/null +++ b/src/service/limit-service/verification/generator/index.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import { VerificationGenerator } from "../common" + +import ConfessionGenerator from "./confession" +import PiGenerator from "./pi" +import UglyGenerator from "./ugly" +import UncommonChinese from "./uncommon-chinese" + +export const ALL_GENERATORS: VerificationGenerator[] = [ + new PiGenerator(), + new ConfessionGenerator(), + new UglyGenerator(), + new UncommonChinese(), +] \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/pi.ts b/src/service/limit-service/verification/generator/pi.ts new file mode 100644 index 000000000..16c5dc9ca --- /dev/null +++ b/src/service/limit-service/verification/generator/pi.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +const MIN_START_IDX = 10 +const MAX_START_IDX = 25 +const MIN_LEN = 5 +const MAX_LEN = 15 + +const DIGIT_PART_PI = '14159265358979323846264338327950288419716939937510' + + '58209749445923078164062862089986280348253421170679' + +/** + * Generator of pi + */ +class PiGenerator implements VerificationGenerator { + generate(_: VerificationContext): VerificationPair { + const startIndex = randomIntBetween(MIN_START_IDX, MAX_START_IDX) + const digitCount = randomIntBetween(MIN_LEN, MAX_LEN) + const endIndex = startIndex + digitCount - 1 + const answer = DIGIT_PART_PI.substring(startIndex - 1, endIndex) + return { + answer, + prompt: msg => msg.pi, + promptParam: { + startIndex, + endIndex, + digitCount, + } + } + } + + supports(context: VerificationContext): boolean { + return context.difficulty === 'disgusting' + } +} + +export default PiGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/ugly.ts b/src/service/limit-service/verification/generator/ugly.ts new file mode 100644 index 000000000..46cdcb21d --- /dev/null +++ b/src/service/limit-service/verification/generator/ugly.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +class UglyGenerator implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'hard' + } + + generate(_: VerificationContext): VerificationPair { + const min = 1 << 6 + const max = 1 << 8 + const random = randomIntBetween(min, max) + return { + answer: random?.toString(2) + } + } +} + +export default UglyGenerator \ No newline at end of file diff --git a/src/service/limit-service/verification/generator/uncommon-chinese.ts b/src/service/limit-service/verification/generator/uncommon-chinese.ts new file mode 100644 index 000000000..c3d42e6a0 --- /dev/null +++ b/src/service/limit-service/verification/generator/uncommon-chinese.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { randomIntBetween } from "@util/number" +import { VerificationContext, VerificationGenerator, VerificationPair } from "../common" + +const UNCOMMON_WORDS = '龘靐齉齾爩鱻麤龗灪吁龖厵滟爨癵籱饢驫鲡鹂鸾麣纞虋讟钃骊郁鸜麷鞻韽韾响顟顠饙饙騳騱饐' +const LENGTH = UNCOMMON_WORDS.length + +class UncommonChinese implements VerificationGenerator { + supports(context: VerificationContext): boolean { + return context.difficulty === 'disgusting' && context.locale === 'zh_CN' + } + + generate(_: VerificationContext): VerificationPair { + let answer = '' + while (answer.length < 3) { + const idx = randomIntBetween(0, LENGTH) + const ch = UNCOMMON_WORDS[idx] + if (!answer.includes(ch)) { + answer += ch + } + } + return { + answer + } + } +} + +export default UncommonChinese \ No newline at end of file diff --git a/src/service/limit-service/verification/processor.ts b/src/service/limit-service/verification/processor.ts new file mode 100644 index 000000000..860261b57 --- /dev/null +++ b/src/service/limit-service/verification/processor.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { VerificationContext, VerificationGenerator, VerificationPair } from "./common" +import { ALL_GENERATORS } from "./generator" + +class VerificationProcessor { + generators: VerificationGenerator[] + + constructor() { + this.generators = ALL_GENERATORS + } + + generate(difficulty: timer.limit.VerificationDifficulty, locale: timer.Locale): VerificationPair { + const context: VerificationContext = { difficulty, locale } + const supported = this.generators.filter(g => g.supports(context)) + const len = supported?.length + if (!len) { + return null + } + let generator = supported[0] + if (len > 1) { + const idx = Math.floor(Math.random() * supported.length) + generator = supported[idx] + } + return generator.generate(context) + } +} + +export default new VerificationProcessor() \ No newline at end of file diff --git a/src/service/option-service.ts b/src/service/option-service.ts index 067d908cf..911693d0f 100644 --- a/src/service/option-service.ts +++ b/src/service/option-service.ts @@ -11,6 +11,7 @@ import { defaultPopup, defaultStatistics, defaultBackup, + defaultDailyLimit, } from "@util/constant/option" const db = new OptionDatabase(chrome.storage.local) @@ -19,6 +20,7 @@ const defaultOption = () => ({ ...defaultAppearance(), ...defaultPopup(), ...defaultStatistics(), + ...defaultDailyLimit(), ...defaultBackup(), }) @@ -41,6 +43,11 @@ async function setStatisticsOption(option: timer.option.StatisticsOption): Promi await setOption(option) } +async function setDailyLimitOption(option: timer.option.DailyLimitOption): Promise { + // Rewrite password + await setOption(option) +} + async function setBackupOption(option: Partial): Promise { // Rewrite auths const existOption = await getAllOption() @@ -92,6 +99,10 @@ class OptionService { setPopupOption = setPopupOption setAppearanceOption = setAppearanceOption setStatisticsOption = setStatisticsOption + /** + * @since 1.9.0 + */ + setDailyLimitOption = setDailyLimitOption /** * @since 1.2.0 */ diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index ba4d7b178..5265b81d9 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -34,7 +34,6 @@ export function defaultAppearance(): timer.option.AppearanceOption { darkModeTimeStart: 64800, // 6*60*60 darkModeTimeEnd: 21600, - limitMarkFilter: 'translucent', } } @@ -46,6 +45,15 @@ export function defaultStatistics(): timer.option.StatisticsOption { } } +export function defaultDailyLimit(): timer.option.DailyLimitOption { + return { + limitLevel: 'nothing', + limitFilter: 'translucent', + limitPassword: '', + limitVerifyDifficulty: 'easy', + } +} + export function defaultBackup(): timer.option.BackupOption { return { backupType: 'none', @@ -62,5 +70,6 @@ export function defaultOption(): timer.option.AllOption { ...defaultAppearance(), ...defaultStatistics(), ...defaultBackup(), + ...defaultDailyLimit(), } } \ No newline at end of file diff --git a/src/util/number.ts b/src/util/number.ts index ca5d02f3e..d8d678888 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -23,3 +23,10 @@ export function tryParseInteger(str: string): [boolean, number | string] { const isInteger: boolean = !isNaN(num) && num.toString().length === str.length return [isInteger, isInteger ? num : str] } + +/** + * Generate random integer between {@param lowerInclusive} and {@param upperExclusive} + */ +export function randomIntBetween(lowerInclusive: number, upperExclusive: number): number { + return Math.floor(Math.random() * (upperExclusive - lowerInclusive)) + lowerInclusive +} \ No newline at end of file diff --git a/types/timer/limit.d.ts b/types/timer/limit.d.ts index 3d5b79d2e..bf4f5027e 100644 --- a/types/timer/limit.d.ts +++ b/types/timer/limit.d.ts @@ -44,4 +44,24 @@ declare namespace timer.limit { | 'translucent' // ground glass filter | 'groundGlass' + /** + * @since 1.9.0 + */ + type RestrictionLevel = + // No additional action required to lock + | 'nothing' + // Password required to lock or modify restricted rule + | 'password' + // Verification code input requird to lock or modify restricted rule + | 'verification' + /** + * @since 1.9.0 + */ + type VerificationDifficulty = + // Easy + | 'easy' + // Need some operations + | 'hard' + // Disgusting + | 'disgusting' } diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts index 5e78e9552..7330ca9b6 100644 --- a/types/timer/option.d.ts +++ b/types/timer/option.d.ts @@ -103,8 +103,9 @@ declare namespace timer.option { /** * The filter of limit mark * @since 1.3.2 + * @deprecated moved to DailyLimitOption @since 1.9.0 */ - limitMarkFilter: limit.FilterType + limitMarkFilter?: limit.FilterType } type StatisticsOption = { @@ -125,6 +126,25 @@ declare namespace timer.option { countLocalFiles: boolean } + type DailyLimitOption = { + /** + * restriction level + */ + limitLevel: limit.RestrictionLevel + /** + * The filter of limit mark + */ + limitFilter: limit.FilterType + /** + * The password to unlock + */ + limitPassword: string + /** + * The difficulty of verification + */ + limitVerifyDifficulty: limit.VerificationDifficulty + } + /** * The options of backup * @@ -153,7 +173,7 @@ declare namespace timer.option { autoBackUpInterval: number } - type AllOption = PopupOption & AppearanceOption & StatisticsOption & BackupOption + type AllOption = PopupOption & AppearanceOption & StatisticsOption & DailyLimitOption & BackupOption /** * @since 0.8.0 */ From e5e58a953aeec79608e3c81865c72a4eb312b123 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 21 Jun 2023 21:57:03 +0800 Subject: [PATCH 164/168] Optimize style of select --- src/app/components/option/components/popup.ts | 35 +++++++++++++++++-- src/i18n/message/app/option-resource.json | 4 --- src/i18n/message/app/option.ts | 1 - 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/app/components/option/components/popup.ts b/src/app/components/option/components/popup.ts index 3887978ed..5f7bdcf94 100644 --- a/src/app/components/option/components/popup.ts +++ b/src/app/components/option/components/popup.ts @@ -9,6 +9,7 @@ import { unref, UnwrapRef } from "vue" import { ElDivider, ElInputNumber, ElOption, ElSelect, ElSwitch } from "element-plus" import { t } from "@app/locale" +import { I18nKey, t as t_ } from "@i18n" import { defineComponent, h, reactive } from "vue" import optionService from "@service/option-service" import { renderOptionItem, tagText } from "../common" @@ -18,6 +19,36 @@ import { ALL_DIMENSIONS } from "@util/stat" import { locale } from "@i18n" import { rotate } from "@util/array" +type LocaleStyle = { + durationSelectWidth: number + typeSelectWidth: number +} + +const STYLES: Messages = { + zh_CN: { + typeSelectWidth: 85, + durationSelectWidth: 80, + }, + en: { + typeSelectWidth: 115, + durationSelectWidth: 110 + }, + ja: { + typeSelectWidth: 85, + durationSelectWidth: 105, + }, + pt_PT: { + typeSelectWidth: 155, + durationSelectWidth: 120, + }, + zh_TW: { + typeSelectWidth: 85, + durationSelectWidth: 80, + }, +} + +const tStyle = (key: I18nKey) => t_(STYLES, { key }) + const mergeDomain = (option: UnwrapRef) => h(ElSwitch, { modelValue: option.defaultMergeDomain, onChange: (newVal: boolean) => { @@ -41,7 +72,7 @@ const typeOptions = () => ALL_DIMENSIONS.map(item => h(ElOption, { value: item, const typeSelect = (option: UnwrapRef) => h(ElSelect, { modelValue: option.defaultType, size: 'small', - style: { width: '120px' }, + style: { width: `${tStyle(m => m.typeSelectWidth)}px` }, onChange: (val: timer.stat.Dimension) => { option.defaultType = val optionService.setPopupOption(unref(option)) @@ -52,7 +83,7 @@ const durationOptions = () => ALL_POPUP_DURATION.map(item => h(ElOption, { value const durationSelect = (option: UnwrapRef) => h(ElSelect, { modelValue: option.defaultDuration, size: 'small', - style: { width: t(msg => msg.option.popup.durationWidth) }, + style: { width: `${tStyle(m => m.durationSelectWidth)}px` }, onChange: (val: PopupDuration) => { option.defaultDuration = val optionService.setPopupOption(unref(option)) diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index a6bf678b6..feab33799 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -8,7 +8,6 @@ "defaultMergeDomain": "{input} 打开时合并子域名", "defaultDisplay": "打开时显示 {duration} {type}", "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名", - "durationWidth": "80px", "weekStart": "每周的第一天 {input}", "weekStartAsNormal": "按照惯例" }, @@ -107,7 +106,6 @@ "defaultMergeDomain": "{input} 打開時合併子域名", "defaultDisplay": "打開時顯示 {duration} {type}", "displaySiteName": "{input} 顯示時是否使用 {siteName} 來代替域名", - "durationWidth": "80px", "weekStart": "每週的第一天 {input}", "weekStartAsNormal": "按照慣例" }, @@ -193,7 +191,6 @@ "defaultMergeDomain": "{input} Whether to merge subdomains on open", "defaultDisplay": "Show {duration} {type} on open", "displaySiteName": "{input} Whether to display {siteName} instead of URL", - "durationWidth": "110px", "weekStart": "The first day for each week {input}", "weekStartAsNormal": "As Normal" }, @@ -293,7 +290,6 @@ "defaultMergeDomain": "{input} オープン時にサブドメインをマージ", "defaultDisplay": "開くと {duration} {type} が表示されます", "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか", - "durationWidth": "100px", "weekStart": "週の最初の日 {input}", "weekStartAsNormal": "いつものように" }, diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 871ad717a..0751d80ea 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -16,7 +16,6 @@ export type OptionMessage = { defaultMergeDomain: string defaultDisplay: string displaySiteName: string - durationWidth: string weekStart: string weekStartAsNormal: string } From e4eaa7ebb571a312c8bb7de2107629abdae15f1d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 22 Jun 2023 00:04:05 +0800 Subject: [PATCH 165/168] Fix npe of source --- script/crowdin/common.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/script/crowdin/common.ts b/script/crowdin/common.ts index 004a475c9..f6df06ac7 100644 --- a/script/crowdin/common.ts +++ b/script/crowdin/common.ts @@ -101,6 +101,10 @@ export async function mergeMessage( return } const sourceText = sourceItemSet[path] + if (!sourceText) { + // Deleted key + return + } if (!checkPlaceholder(text, sourceText)) { console.error(`Invalid placeholder: dir=${dir}, filename=${filename}, path=${path}, source=${sourceText}, translated=${text}`) return From 6001231fc95d91441b156e91fa1ae51568254bab Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 22 Jun 2023 00:04:41 +0800 Subject: [PATCH 166/168] Downloadd translations --- src/i18n/message/app/limit-resource.json | 40 ++++++++++++++--- src/i18n/message/app/option-resource.json | 44 ++++++++++++++++++- .../message/app/site-manage-resource.json | 3 +- .../message/common/calendar-resource.json | 3 +- 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index ad3ae6825..767acbbfa 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -53,7 +53,8 @@ "operation": "操作" }, "button": { - "test": "網址測試" + "test": "網址測試", + "option": "設置" }, "addTitle": "新增限製", "useWildcard": "是否使用通配符", @@ -69,7 +70,16 @@ "noRuleMatched": "該網址未命中任何規則", "rulesMatched": "該網址命中以下規則:" }, - "urlPlaceholder": "請直接粘貼網址 ➡️" + "urlPlaceholder": "請直接粘貼網址 ➡️", + "verification": { + "inputTip": "該規則已超時,如要表更,請輸入以下問題的答案:{prompt}", + "inputTip2": "該規則已超時,如要表更,請在下列輸入框中原樣輸入:{answer}", + "pswInputTip": "該規則已超時,如要表更,所以請在下列輸入框中輸入您的解鎖密碼", + "incorrectPsw": "密碼錯誤", + "incorrectAnswer": "答案不正確", + "pi": "圓周率 π 的小數部分第 {startIndex} 位到第 {endIndex} 位的共 {digitCount} 位數字", + "confession": "一寸光一寸金,寸金難買寸光陰" + } }, "en": { "conditionFilter": "URL", @@ -125,7 +135,8 @@ "operation": "操作" }, "button": { - "test": "テスト URL" + "test": "テストURL", + "option": "設定" }, "addTitle": "新增", "useWildcard": "ワイルドカードを使用するかどうか", @@ -141,7 +152,16 @@ "noRuleMatched": "URL がどのルールとも一致しません", "rulesMatched": "URL は次のルールに一致します。" }, - "urlPlaceholder": "URLを直接貼り付けてください➡️" + "urlPlaceholder": "URLを直接貼り付けてください➡️", + "verification": { + "inputTip": "このルールは既にトリガーされています。変更するには、次のプロンプトに対する回答を入力してください: {prompt}", + "inputTip2": "このルールは既にトリガーされています。変更するには以下のように入力してください: {answer}", + "pswInputTip": "このルールは既にトリガーされています。変更するにはロック解除パスワードを入力してください。", + "incorrectPsw": "間違ったパスワード", + "incorrectAnswer": "間違った回答", + "pi": "πの小数部の{digitCount} から {startIndex} までの {endIndex} 桁の数", + "confession": "人生とは今日一日のことである" + } }, "pt_PT": { "conditionFilter": "URL", @@ -159,7 +179,8 @@ "operation": "Operações" }, "button": { - "test": "Testar URL" + "test": "Testar URL", + "option": "Opções" }, "message": { "saved": "Guardado com sucesso", @@ -172,6 +193,15 @@ "clickTestButton": "Após a entrada, por favor, clique no botão ({buttonText})", "noRuleMatched": "O URL não atinge nenhuma regra", "rulesMatched": "A URL atinge as seguintes regras:" + }, + "verification": { + "inputTip": "Esta regra já foi acionada. Para modificá-la, por favor, digite a resposta para o seguinte promotor: {prompt}", + "inputTip2": "Esta regra já foi acionada. Para modificá-la, por favor insira-a como está: {answer}", + "pswInputTip": "Esta regra já foi acionada. Para modificá-la, digite a sua senha de desbloqueio", + "incorrectPsw": "Palavra-passe incorreta", + "incorrectAnswer": "Resposta incorreta", + "pi": "{digitCount} dígitos de {startIndex} a {endIndex} da parte decimal de π", + "confession": "Tempo é dinheiro" } } } \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index feab33799..0b1193090 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -155,6 +155,20 @@ "label": "模態頁面的背景風格 {input}", "translucent": "半透明", "groundGlass": "毛玻璃" + }, + "title": "每日時限", + "level": { + "label": "受限時如何解鎖 {input}", + "nothing": "允許在管理頁面直接解鎖", + "password": "必須輸入密碼解鎖", + "verification": "必須輸入驗證碼解鎖", + "passwordLabel": "解鎖密碼 {input}", + "verificationLabel": "驗證碼的難度 {input}", + "verificationDifficulty": { + "easy": "簡單", + "hard": "困難", + "disgusting": "噁心" + } } }, "backup": { @@ -336,9 +350,23 @@ }, "dailyLimit": { "filter": { - "label": "モーダルページの背景スタイル {input}", + "label": "モーダルページ {input} の背景スタイル", "translucent": "半透明", "groundGlass": "すりガラス" + }, + "title": "閲覧の制限", + "level": { + "label": "制限中のロック解除方法 {input}", + "nothing": "管理者ページでの直接のロック解除を許可する", + "password": "ロックを解除するにはパスワードを入力してください", + "verification": "ロックを解除するには認証コードを入力する必要があります", + "passwordLabel": "ロックを解除するためのパスワード {input}", + "verificationLabel": "認証コードの難しさ {input}", + "verificationDifficulty": { + "easy": "簡単", + "hard": "難しい", + "disgusting": "気持ち悪い" + } } }, "backup": { @@ -427,6 +455,20 @@ "label": "O estilo de fundo da página modal {input}", "translucent": "Translúcido", "groundGlass": "Vidro no Solo" + }, + "title": "Limite diário", + "level": { + "label": "Como desbloquear enquanto restrito {input}", + "nothing": "Permitir o desbloqueio direto na página de administração", + "password": "É necessário digitar a senha para desbloquear", + "verification": "É necessário digitar o código de verificação para desbloquear", + "passwordLabel": "Senha para desbloquear {input}", + "verificationLabel": "A dificuldade do código de verificação {input}", + "verificationDifficulty": { + "easy": "Fácil", + "hard": "Difícil", + "disgusting": "Nojento" + } } }, "backup": { diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index becce9ebf..d1c40b5a0 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -91,7 +91,8 @@ "hostExistWarn": "{host} 已經存在", "saved": "已保存", "existedTag": "已存在", - "mergedTag": "合並" + "mergedTag": "合並", + "virtualTag": "虛擬" } }, "en": { diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index 74df2886c..8aa45a4f9 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -100,7 +100,8 @@ "last15Days": "Últimos 15 dias", "last30Days": "Últimos 30 dias", "last60Days": "Últimos 60 dias", - "last90Days": "Últimos 90 dias" + "last90Days": "Últimos 90 dias", + "last24Hours": "Últimas 24 horas" } } } \ No newline at end of file From 622bee3e2317a6fde3f71d40501eb1d8b2913745 Mon Sep 17 00:00:00 2001 From: qcloud Date: Tue, 27 Jun 2023 16:19:24 +0800 Subject: [PATCH 167/168] Support search with alias (#214) --- .../components/analysis/components/filter.ts | 56 ++++++++++++------- src/database/site-database.ts | 16 ++++-- src/service/site-service.ts | 6 ++ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/app/components/analysis/components/filter.ts b/src/app/components/analysis/components/filter.ts index 3d2db2453..07c1f10bf 100644 --- a/src/app/components/analysis/components/filter.ts +++ b/src/app/components/analysis/components/filter.ts @@ -10,23 +10,36 @@ import type { Ref, PropType, VNode } from "vue" import { ElOption, ElSelect, ElTag } from "element-plus" import { ref, h, defineComponent } from "vue" import statService, { HostSet } from "@service/stat-service" +import siteService from "@service/site-service" import { t } from "@app/locale" import SelectFilterItem from "@app/components/common/select-filter-item" import { labelOfHostInfo } from "../util" -async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { +const calcUniqueKey = ({ host, virtual, merged }: timer.site.SiteInfo) => `${host}${virtual ? 1 : 0}${merged ? 1 : 0}` + +async function handleRemoteSearch(queryStr: string, trendDomainOptions: Ref, searching: Ref) { if (!queryStr) { trendDomainOptions.value = [] return } searching.value = true - const domains: HostSet = await statService.listHosts(queryStr) - const options: timer.site.SiteKey[] = [] - 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 + + const options: Record = {} + const sites = await siteService.selectAll({ fuzzyQuery: queryStr }) + const hosts: HostSet = await statService.listHosts(queryStr) + + sites.forEach(site => options[calcUniqueKey(site)] = site) + + const { origin, merged, virtual } = hosts + const originSiteInfo: timer.site.SiteInfo[] = [] + origin.forEach(host => originSiteInfo.push({ host })) + merged.forEach(host => originSiteInfo.push({ host, merged: true })) + virtual.forEach(host => originSiteInfo.push({ host, virtual: true })) + originSiteInfo.forEach(o => { + const key = calcUniqueKey(o) + !options[key] && (options[key] = o) + }) + trendDomainOptions.value = Object.values(options) searching.value = false } @@ -55,16 +68,21 @@ function hostInfoOfKey(key: string): timer.site.SiteKey { const MERGED_TAG_TXT = t(msg => msg.analysis.common.merged) const VIRTUAL_TAG_TXT = t(msg => msg.analysis.common.virtual) -function renderHostLabel(hostInfo: timer.site.SiteKey): VNode[] { + +const renderOptionTag = (tagLabel: string) => h('span', + { style: { float: "right", height: "34px" } }, + h(ElTag, { size: 'small' }, () => tagLabel) +) + +function renderHostLabel({ host, merged, virtual, alias }: timer.site.SiteInfo): VNode[] { const result = [ - h('span', {}, hostInfo.host) + h('span', {}, host) ] - hostInfo.merged && result.push( - h(ElTag, { size: 'small' }, () => MERGED_TAG_TXT) - ) - hostInfo.virtual && result.push( - h(ElTag, { size: 'small' }, () => VIRTUAL_TAG_TXT) + alias && result.push( + h(ElTag, { size: 'small', type: 'info' }, () => alias) ) + merged && result.push(renderOptionTag(MERGED_TAG_TXT)) + virtual && result.push(renderOptionTag(VIRTUAL_TAG_TXT)) return result } @@ -113,10 +131,10 @@ const _default = defineComponent({ handleSiteChange() } }, () => (trendDomainOptions.value || [])?.map( - hostInfo => h(ElOption, { - value: keyOfHostInfo(hostInfo), - label: labelOfHostInfo(hostInfo), - }, () => renderHostLabel(hostInfo)) + siteInfo => h(ElOption, { + value: keyOfHostInfo(siteInfo), + label: labelOfHostInfo(siteInfo), + }, () => renderHostLabel(siteInfo)) )), h(SelectFilterItem, { historyName: 'timeFormat', diff --git a/src/database/site-database.ts b/src/database/site-database.ts index 4f4265c01..7fe50b2e1 100644 --- a/src/database/site-database.ts +++ b/src/database/site-database.ts @@ -11,6 +11,10 @@ import { REMAIN_WORD_PREFIX } from "./common/constant" export type SiteCondition = { host?: string alias?: string + /** + * Fuzzy query of host or alias + */ + fuzzyQuery?: string source?: timer.site.AliasSource virtual?: boolean } @@ -95,16 +99,18 @@ async function select(this: SiteDatabase, condition?: SiteCondition): Promise boolean { - const { host, alias, source, virtual } = condition || {} + const { host, alias, source, virtual, fuzzyQuery } = 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 + const { host: siteHost, alias: siteAlias, source: siteSource, virtual: siteVirtual } = site || {} + if (host && !siteHost.includes(host)) return false + if (alias && !siteAlias?.includes(alias)) return false + if (source && source !== siteSource) return false if (virtual !== undefined && virtual !== null) { const virtualCond = virtual || false - const virtualFactor = site.virtual || false + const virtualFactor = siteVirtual || false if (virtualCond !== virtualFactor) return false } + if (fuzzyQuery && !(siteHost?.includes(fuzzyQuery) || siteAlias?.includes(fuzzyQuery))) return false return true } } diff --git a/src/service/site-service.ts b/src/service/site-service.ts index aad393fb8..3df9a05bd 100644 --- a/src/service/site-service.ts +++ b/src/service/site-service.ts @@ -6,10 +6,12 @@ */ import SiteDatabase, { SiteCondition } from "@db/site-database" +import StatDatabase from "@db/stat-database" import { slicePageResult } from "./components/page-info" const storage = chrome.storage.local const siteDatabase = new SiteDatabase(storage) +const statDatabase = new StatDatabase(storage) export type SiteQueryParam = SiteCondition @@ -63,6 +65,10 @@ class SiteService { return result } + selectAll(param?: SiteQueryParam): Promise { + return siteDatabase.select(param) + } + async batchSelect(keys: timer.site.SiteKey[]): Promise { return siteDatabase.getBatch(keys) } From 340546acadbb8cade8e9af0c61afcc2adccc5184 Mon Sep 17 00:00:00 2001 From: qcloud Date: Tue, 27 Jun 2023 16:20:29 +0800 Subject: [PATCH 168/168] v1.9.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6ba7adee9..b71edcc80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "1.8.2", + "version": "1.9.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { @@ -68,4 +68,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +}