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/.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
diff --git a/README-zh.md b/README-zh.md
index c69d8e463..eafb64902 100644
--- a/README-zh.md
+++ b/README-zh.md
@@ -10,7 +10,6 @@
网费很贵是一款用于上网时间统计的浏览器插件,使用 webpack,TypeScript 和 Element-plus 进行开发。你可以在 Firefox,Chrome 和 Edge 中安装并使用它。
-- 统计网站的运行时间
- 统计用户在不同网站上的浏览时间,和访问次数
- 统计用户阅读本地文件的时间
- 网站白名单,过滤不需要统计的网站
@@ -51,10 +50,6 @@
----
-
-详细展示图文:[douban.com](https://www.douban.com/group/topic/213888429/)
-
## 贡献指南
如果你想参与到该项目的开源建设,可以考虑以下几种方式
@@ -73,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 e24722cf3..947320ca8 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.
@@ -55,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
diff --git a/global.d.ts b/global.d.ts
index 9a1fcdcab..665963661 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -6,7 +6,9 @@
*/
declare namespace timer {
namespace option {
-
+ type PopupDuration =
+ | "today" | "thisWeek" | "thisMonth"
+ | "last30Days"
/**
* Options used for the popup page
*/
@@ -23,7 +25,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
@@ -143,6 +145,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 +175,12 @@ declare namespace timer {
* @since 1.2.0
*/
cid?: string
+ backup?: {
+ [key in timer.backup.Type]?: {
+ ts: number
+ msg?: string
+ }
+ }
}
}
@@ -201,15 +217,23 @@ declare namespace timer {
| 'fr'
| 'it'
| 'sv'
+ | 'fi'
+ | 'da'
+ | 'hr'
+ | 'id'
+ | 'tr'
+ | 'cs'
+ | 'ro'
+ | 'nl'
+ | 'vi'
+ | 'sk'
+ | 'mn'
namespace stat {
/**
* The dimension to statistics
*/
type Dimension =
- // Running time
- // @deprecated v1.3.4
- | 'total'
// Focus time
| 'focus'
// Visit count
@@ -252,6 +276,10 @@ declare namespace timer {
* @since 0.1.5
*/
mergedHosts: Row[]
+ /**
+ * The composition of data when querying remote
+ */
+ composition?: RemoteComposition
/**
* Icon url
*
@@ -262,34 +290,46 @@ 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[]
}
}
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
*/
@@ -398,24 +438,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
@@ -425,60 +447,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
- }
- }
}
/**
@@ -553,7 +521,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/package.json b/package.json
index 912720fd9..cd20d9305 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "timer",
- "version": "1.4.0",
+ "version": "1.5.0",
"description": "Web timer",
"homepage": "https://github.com/sheepzh/timer",
"scripts": {
@@ -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/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 34ebe1ca0..7b8f3c446 100644
--- a/script/user-chart/render.ts
+++ b/script/user-chart/render.ts
@@ -7,13 +7,13 @@ 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']
+const POINT_COUNT = 200
+
type OriginData = {
[browser in Browser]: UserCount
}
@@ -25,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()
@@ -64,7 +51,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
@@ -136,10 +123,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: {
@@ -210,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
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/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..6f17c6741 100644
--- a/src/app/components/common/switch-filter-item.ts
+++ b/src/app/components/common/switch-filter-item.ts
@@ -5,43 +5,37 @@
* 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({
name: "SwitchFilterItem",
+ emits: {
+ change: (_val: boolean) => true
+ },
props: {
+ label: String,
defaultValue: {
type: Boolean,
- default: false
+ required: false,
},
- /**
- * Whether to save the value in the localstorage with {@param historyName}
- */
historyName: {
type: String,
- required: false
- },
- label: String
+ required: false,
+ }
},
- 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 00a2ca8f3..1bdc5e277 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,
@@ -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/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/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/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 086257f2c..8218d6988 100644
--- a/src/app/components/data-manage/clear/filter/index.ts
+++ b/src/app/components/data-manage/clear/filter/index.ts
@@ -5,67 +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 totalStartRef: Ref = ref('0')
-const totalEndRef: Ref = ref('')
-const timeStartRef: Ref = ref('0')
-const timeEndRef: Ref = ref('')
-
-const title = h('h3', t(msg => msg.dataManage.filterItems))
-
-const filterRefs: BaseFilterProps = {
- dateRangeRef,
- focusStartRef, focusEndRef,
- totalStartRef, totalEndRef,
- 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('filterTotal', totalStartRef, totalEndRef, 3),
- numberFilter('filterTime', timeStartRef, timeEndRef, 4),
- 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/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/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/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/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/help-us/progress-list.ts b/src/app/components/help-us/progress-list.ts
index 0e820c9b7..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 } = {}
@@ -62,7 +73,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/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
diff --git a/src/app/components/limit/filter.ts b/src/app/components/limit/filter.ts
index 7b149e841..dd8ef2385 100644
--- a/src/app/components/limit/filter.ts
+++ b/src/app/components/limit/filter.ts
@@ -5,21 +5,23 @@
* 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"
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",
@@ -27,14 +29,14 @@ const _default = defineComponent({
url: String,
onlyEnabled: Boolean
},
- emits: ["create", "change"],
+ 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,
@@ -52,6 +54,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 52074f917..bb7f7dc9b 100644
--- a/src/app/components/limit/index.ts
+++ b/src/app/components/limit/index.ts
@@ -7,13 +7,15 @@
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 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"
import { ElMessage } from "element-plus"
+import { handleWindowVisibleChange } from "@util/window"
const _default = defineComponent({
name: "Limit",
@@ -27,12 +29,15 @@ 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: {} })
urlParam && (url.value = decodeURIComponent(urlParam))
const modify: Ref = ref()
+ const test: Ref = ref()
return () => h(ContentContainer, {}, {
filter: () => h(LimitFilter, {
@@ -43,23 +48,30 @@ const _default = defineComponent({
onlyEnabled.value = option.onlyEnabled
queryData()
},
- onCreate: () => modify.value?.show?.()
+ 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.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))
queryData()
+ },
+ async onModify(row: timer.limit.Item) {
+ modify.value?.modify?.(row)
}
}),
- h(Modify, {
+ h(LimitModify, {
ref: modify,
onSave: queryData
- })
+ }),
+ h(LimitTest, {
+ ref: 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/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..0296d1a04 100644
--- a/src/app/components/limit/modify/form/index.ts
+++ b/src/app/components/limit/modify/form/index.ts
@@ -6,74 +6,100 @@
*/
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..8ca5cdb51 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,40 @@ 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: (_url: string) => true
+ },
+ 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..14a5042a9 100644
--- a/src/app/components/limit/modify/form/time-limit.ts
+++ b/src/app/components/limit/modify/form/time-limit.ts
@@ -5,51 +5,86 @@
* 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: (_val: number) => true
+ },
+ 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..b595c63d6 100644
--- a/src/app/components/limit/modify/form/url.ts
+++ b/src/app/components/limit/modify/form/url.ts
@@ -5,49 +5,57 @@
* https://opensource.org/licenses/MIT
*/
-import { h, Ref } 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"
-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 protocolOptions = () => ALL_PROTOCOLS.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 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) => {
+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)
if (!granted) {
@@ -67,46 +75,60 @@ 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 pasteButtonText = t(msg => msg.limit.button.paste)
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: (_val: string) => true,
+ protocolChange: (_val: Protocol) => true,
+ },
+ 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_: _Slots = slots(protocolRef, urlRef, props.disabled)
+ 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..78994723b 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"
@@ -17,13 +17,29 @@ 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()
+ 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 +52,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 +61,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/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 efbade6aa..a42e68094 100644
--- a/src/app/components/limit/table/column/operation.ts
+++ b/src/app/components/limit/table/column/operation.ts
@@ -5,21 +5,25 @@
* 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: (_row: timer.limit.Item, _cond: string) => true,
+ rowModify: (_row: timer.limit.Item) => true,
+ },
setup(_props, ctx) {
return () => h(ElTableColumn, {
prop: 'operations',
label,
- minWidth: 80,
+ minWidth: 200,
align: 'center',
}, {
default: ({ row }: { row: timer.limit.Item }) => [
@@ -34,7 +38,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..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"],
+ 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,14 @@ 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/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/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/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/option/components/popup.ts b/src/app/components/option/components/popup.ts
index 036e5b7af..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))
}
@@ -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/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/app/components/report/file-export.ts b/src/app/components/report/file-export.ts
index 9ac26cd80..89dd325aa 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
}
@@ -25,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) {
@@ -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
@@ -57,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) {
@@ -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)
@@ -100,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/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 d5d7b3d74..308b4eb55 100644
--- a/src/app/components/report/index.ts
+++ b/src/app/components/report/index.ts
@@ -7,16 +7,14 @@
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"
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"
+import ReportTable from "./table"
import ReportFilter from "./filter"
import Pagination from "../common/pagination"
import ContentContainer from "../common/content-container"
@@ -28,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)
@@ -122,7 +121,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 +166,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 +175,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 +184,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 ? SortDirect.ASC : SortDirect.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 +214,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([])
@@ -227,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)
@@ -237,7 +238,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 +247,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()
@@ -259,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/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/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/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/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/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..17aee7099 100644
--- a/src/app/components/report/table/index.ts
+++ b/src/app/components/report/table/index.ts
@@ -8,26 +8,15 @@
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"
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"
-export enum ElSortDirect {
- ASC = 'ascending',
- DESC = 'descending'
-}
-
-export type SortInfo = {
- prop: timer.stat.Dimension | 'host'
- order: ElSortDirect
-}
-
const _default = defineComponent({
name: "ReportTable",
props: {
@@ -37,11 +26,19 @@ 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,
+ aliasChange: (_host: string, _newAlias: string) => true,
+ itemDelete: (_row: timer.stat.Row) => true,
+ whitelistChange: (_host: string, _addOrRemove: boolean) => true,
},
- emits: ["sortChange", "aliasChange", "itemDelete", "whitelistChange"],
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 || []
@@ -62,21 +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(TotalColumn, { timeFormat: props.timeFormat }))
- 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/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/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/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..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)
@@ -63,7 +76,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 d059e1596..fc108689c 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<
@@ -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: [{
@@ -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[]
@@ -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/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..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"
@@ -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
@@ -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),
@@ -57,32 +58,45 @@ 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 }
}
+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: {
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 +104,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
@@ -117,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/components/trend/index.ts b/src/app/components/trend/index.ts
index 6984280da..287bfa5bf 100644
--- a/src/app/components/trend/index.ts
+++ b/src/app/components/trend/index.ts
@@ -13,14 +13,14 @@ 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
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 []
@@ -41,7 +41,7 @@ async function query(hostOption: Ref, 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/app/layout/menu.ts b/src/app/layout/menu.ts
index 994c52013..3afe77570 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,10 +43,14 @@ type _RouteProps = {
*/
function generateMenus(): _MenuGroup[] {
const otherMenuItems: _MenuItem[] = [{
+ title: 'userManual',
+ href: getGuidePageUrl(false),
+ icon: Memo,
+ index: '_guide',
+ }, {
title: 'helpUs',
route: '/other/help',
icon: HelpFilled,
- index: '_i18n'
}]
HOME_PAGE && otherMenuItems.push({
title: 'rate',
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
diff --git a/src/background/alarm-manager.ts b/src/background/alarm-manager.ts
new file mode 100644
index 000000000..2abb14e56
--- /dev/null
+++ b/src/background/alarm-manager.ts
@@ -0,0 +1,78 @@
+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
+
+const getInnerName = (outerName: string) => ALARM_PREFIX + outerName
+const getOuterName = (innerName: string) => innerName.substring(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 innerName = getOuterName(name)
+ const config: _AlarmConfig = this.alarms[innerName]
+ if (!config) {
+ // Not registered, or removed
+ return
+ }
+ // Handle alarm event
+ 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 })
+ })
+ })
+ }
+
+ /**
+ * 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[outerName]) {
+ // Existed, only update the config
+ this.alarms[outerName] = config
+ return
+ }
+ // Initialize config
+ this.alarms[outerName] = config
+ // Create new one alarm
+ chrome.alarms.create(getInnerName(outerName), { when: Date.now() + interval })
+ }
+
+ /**
+ * Remove a interval
+ */
+ remove(outerName: string) {
+ delete this.alarms[outerName]
+ chrome.alarms.clear(getInnerName(outerName))
+ }
+}
+
+export default new AlarmManager()
\ No newline at end of file
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/badge-text-manager.ts b/src/background/badge-text-manager.ts
index 87c05dfc5..5c993e6b3 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)
@@ -55,66 +56,56 @@ 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 })
- }
- })
}))
}
-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)
-}
-
-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 })
+ return badgeLocation
}
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())
}
/**
@@ -123,7 +114,7 @@ class BadgeTextManager {
async pause() {
this.isPaused = true
const tab = await findActiveTab()
- setBadgeText('P', tab?.tabId)
+ setBadgeText('', tab?.tabId)
}
/**
@@ -135,9 +126,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..6abeb382d 100644
--- a/src/background/index.ts
+++ b/src/background/index.ts
@@ -19,6 +19,8 @@ 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"
+import BackupScheduler from "./backup-scheduler"
// Open the log of console
openLog()
@@ -40,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
@@ -54,6 +59,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") {
@@ -64,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/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/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))
}
}
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/background/backup/gist/compressor.ts b/src/common/backup/gist/compressor.ts
similarity index 88%
rename from src/background/backup/gist/compressor.ts
rename to src/common/backup/gist/compressor.ts
index d8c291a01..661a8985c 100644
--- a/src/background/backup/gist/compressor.ts
+++ b/src/common/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/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 90%
rename from src/background/backup/gist/gist.d.ts
rename to src/common/backup/gist/gist.d.ts
index cb5840ff8..d3ec491f8 100644
--- a/src/background/backup/gist/gist.d.ts
+++ b/src/common/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/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/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
diff --git a/src/content-script/limit.ts b/src/content-script/limit.ts
index 735b48d02..282dd9631 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)
@@ -77,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
@@ -87,7 +90,7 @@ class _Modal {
}
hideModal() {
- if (!this.visible || document.body) {
+ if (!this.visible || !document.body) {
return
}
this.mask.remove()
@@ -150,7 +153,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))
@@ -194,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) {
@@ -218,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/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/limit-database.ts b/src/database/limit-database.ts
index 2da07e591..6949f1016 100644
--- a/src/database/limit-database.ts
+++ b/src/database/limit-database.ts
@@ -74,12 +74,22 @@ class LimitDatabase extends BaseDatabase {
async save(data: timer.limit.Rule, rewrite?: boolean): Promise {
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/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/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/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 4308dbfad..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,
@@ -45,6 +45,18 @@ const advanced = () => [
),
]
+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(
@@ -52,6 +64,7 @@ const _default = defineComponent({
...quickstart(),
...background(),
...advanced(),
+ ...backup(),
)
}
})
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/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/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 025e816b4..21db6fd1c 100644
--- a/src/guide/layout/menu.ts
+++ b/src/guide/layout/menu.ts
@@ -6,28 +6,26 @@
*/
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"
-
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,11 +36,14 @@ 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: 'usage.backup',
+ }
],
icon: Memo
},
@@ -53,18 +54,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,
@@ -72,11 +73,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)),
@@ -90,9 +90,11 @@ function renderGroup(handleClick: (position: string) => void, group: _Group): VN
const _default = defineComponent({
name: "GuideMenu",
- emits: ['click'],
+ emits: {
+ click: (_position: Position) => true,
+ },
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/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/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
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/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/app/data-manage.ts b/src/i18n/message/app/data-manage.ts
index c7f36eb81..91023047f 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
@@ -36,10 +35,9 @@ const _default: Messages = {
totalMemoryAlert: '浏览器为每个扩展提供 {size}MB 来存储本地数据',
totalMemoryAlert1: '无法确定浏览器允许的最大可用内存',
usedMemoryAlert: '当前已使用 {size}MB',
- operationAlert: '您可以归档或者删除那些无关紧要的数据,来减小内存空间',
+ operationAlert: '您可以删除那些无关紧要的数据,来减小内存空间',
filterItems: '数据筛选',
filterFocus: '当日阅览时间在 {start} 秒至 {end} 秒之间。',
- filterTotal: '当日运行时间在 {start} 秒至 {end} 秒之间。',
filterTime: '当日打开次数在 {start} 次至 {end} 次之间。',
filterDate: '{picker} 产生的数据。',
unlimited: '无限',
@@ -61,10 +59,9 @@ const _default: Messages = {
totalMemoryAlert: '瀏覽器爲每個擴充提供 {size}MB 來存儲本地數據',
totalMemoryAlert1: '無法確定瀏覽器允許的最大可用內存',
usedMemoryAlert: '當前已使用 {size}MB',
- operationAlert: '您可以歸檔或者刪除那些無關緊要的數據,來減小內存空間',
+ 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: '∞',
@@ -111,10 +107,9 @@ const _default: Messages = {
totalMemoryAlert: 'ブラウザは、データを保存するために各拡張機能に {size}MB のメモリを提供します',
totalMemoryAlert1: 'ブラウザで許可されている各拡張機能で使用可能な最大メモリを特定できません',
usedMemoryAlert: '現在 {size}MB が使用されています',
- operationAlert: '不要なデータを削除してメモリ容量を減らすことができます',
+ operationAlert: 'これらの重要でないデータを削除して、メモリ使用量を減らすことができます',
filterItems: 'データフィルタリング',
filterFocus: '当日の閲覧時間は、{start} 秒から {end} 秒の間です。',
- filterTotal: '当日の実行時間は、{start} 秒から {end} 秒の間です。',
filterTime: '当日のオープン数は {start} から {end} の間です。',
filterDate: '{picker} までに生成されたデータ',
unlimited: '無制限',
diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts
index 8d91c659d..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,14 +23,11 @@ export type LimitMessage = {
button: {
add: string
test: string
+ testSimple: string
paste: string
save: string
delete: string
- }
- timeUnit: {
- hour: string
- minute: string
- second: string
+ modify: string
}
message: {
noUrl: string
@@ -40,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 = {
@@ -59,17 +60,14 @@ const _default: Messages = {
button: {
add: '新增',
test: '网址测试',
+ testSimple: '测试',
paste: '粘贴',
save: '保存',
delete: '删除',
+ modify: '修改',
},
addTitle: '新增限制',
useWildcard: '是否使用通配符',
- timeUnit: {
- hour: '小时',
- minute: '分钟',
- second: '秒',
- },
message: {
saved: '保存成功',
noUrl: '未填写限制网址',
@@ -77,7 +75,12 @@ const _default: Messages = {
deleteConfirm: '是否删除限制:{cond}?',
deleted: '删除成功',
noPermissionFirefox: '请先在插件管理页[about:addons]开启该插件的粘贴板权限',
+ inputTestUrl: '请先输入需要测试的网址链接',
+ clickTestButton: '输入完成后请点击【{buttonText}】按钮',
+ noRuleMatched: '该网址未命中任何规则',
+ rulesMatched: '该网址命中以下规则:',
},
+ testUrlLabel: '测试网址',
urlPlaceholder: '请直接粘贴网址 ➡️',
},
zh_TW: {
@@ -95,17 +98,14 @@ const _default: Messages = {
button: {
add: '新增',
test: '網址測試',
+ testSimple: '測試',
paste: '粘貼',
save: '保存',
delete: '刪除',
+ modify: '修改',
},
addTitle: '新增限製',
useWildcard: '是否使用通配符',
- timeUnit: {
- hour: '小時',
- minute: '分鐘',
- second: '秒',
- },
message: {
saved: '保存成功',
noUrl: '未填冩限製網址',
@@ -113,8 +113,13 @@ const _default: Messages = {
deleteConfirm: '是否刪除限製:{cond}?',
deleted: '刪除成功',
noPermissionFirefox: '請先在插件管理頁[about:addons]開啟該插件的粘貼闆權限',
+ inputTestUrl: '請先輸入需要測試的網址鏈接',
+ clickTestButton: '輸入完成後請點擊【{buttonText}】按鈕',
+ noRuleMatched: '該網址未命中任何規則',
+ rulesMatched: '該網址命中以下規則:',
},
urlPlaceholder: '請直接粘貼網址 ➡️',
+ testUrlLabel: '測試網址',
},
en: {
conditionFilter: 'URL',
@@ -131,17 +136,14 @@ const _default: Messages = {
button: {
add: 'New',
test: 'Test URL',
+ testSimple: 'Test',
paste: 'Paste',
save: 'Save',
delete: 'Delete',
+ modify: 'Modify',
},
addTitle: 'New',
useWildcard: 'Whether to use wildcard',
- timeUnit: {
- hour: 'Hours',
- minute: 'Minutes',
- second: 'Seconds',
- },
message: {
saved: 'Saved successfully',
noUrl: 'Unfilled limited URL',
@@ -149,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',
@@ -167,17 +174,14 @@ const _default: Messages