From eec57807977a4a9f9d9bfdd939e56df8005e0bca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:41:04 +0800 Subject: [PATCH 001/298] build(deps-dev): bump @types/chrome from 0.1.0 to 0.1.1 (#511) Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.0 to 0.1.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome) --- updated-dependencies: - dependency-name: "@types/chrome" dependency-version: 0.1.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a9eb3db8..6abcf5e7c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@rspack/core": "^1.3.15", "@swc/core": "^1.12.5", "@swc/jest": "^0.2.38", - "@types/chrome": "0.1.0", + "@types/chrome": "0.1.1", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.0.3", From ae389b041c8794a8511ae93a81c28ef36adfa705 Mon Sep 17 00:00:00 2001 From: sheepie Date: Tue, 22 Jul 2025 16:52:21 +0800 Subject: [PATCH 002/298] fix(limit): modal style error and visit time blocking (#513) --- package.json | 26 +++++----- .../limit/modal/components/Footer.tsx | 14 +++--- src/content-script/limit/modal/context.ts | 6 +-- src/content-script/limit/modal/index.ts | 17 +++++-- src/pages/app/components/About/index.tsx | 17 ++++--- src/pages/app/components/HelpUs/index.tsx | 43 +++++++++-------- src/pages/app/components/RuleMerge/index.tsx | 25 +++++----- src/pages/app/components/Whitelist/index.tsx | 39 ++++++++------- test-e2e/limit/common.ts | 18 +++++-- test-e2e/limit/visit-limit.test.ts | 47 +++++++++++++++++++ 10 files changed, 156 insertions(+), 96 deletions(-) create mode 100644 test-e2e/limit/visit-limit.test.ts diff --git a/package.json b/package.json index 6abcf5e7c..6c3225c6c 100644 --- a/package.json +++ b/package.json @@ -29,17 +29,17 @@ "license": "MIT", "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/preset-env": "^7.27.2", - "@crowdin/crowdin-api-client": "^1.45.0", - "@rsdoctor/rspack-plugin": "^1.1.4", - "@rspack/cli": "^1.3.15", - "@rspack/core": "^1.3.15", - "@swc/core": "^1.12.5", - "@swc/jest": "^0.2.38", + "@babel/preset-env": "^7.28.0", + "@crowdin/crowdin-api-client": "^1.46.0", + "@rsdoctor/rspack-plugin": "^1.1.8", + "@rspack/cli": "^1.4.8", + "@rspack/core": "^1.4.8", + "@swc/core": "^1.13.1", + "@swc/jest": "^0.2.39", "@types/chrome": "0.1.1", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.0.3", + "@types/node": "^24.0.15", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.4.0", "babel-loader": "^10.0.0", @@ -47,13 +47,13 @@ "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", - "jest": "^30.0.2", - "jest-environment-jsdom": "^30.0.2", + "jest": "^30.0.4", + "jest-environment-jsdom": "^30.0.4", "jest-junit": "^16.0.0", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.10.2", + "puppeteer": "^24.14.0", "sass": "^1.89.2", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", @@ -64,11 +64,11 @@ "url-loader": "^4.1.1" }, "optionalDependencies": { - "web-ext": "^8.8.0" + "web-ext": "^8.9.0" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", - "@vueuse/core": "^13.4.0", + "@vueuse/core": "^13.5.0", "countup.js": "^2.9.0", "echarts": "^5.6.0", "element-plus": "2.10.4", diff --git a/src/content-script/limit/modal/components/Footer.tsx b/src/content-script/limit/modal/components/Footer.tsx index 5cf603e34..bb741df15 100644 --- a/src/content-script/limit/modal/components/Footer.tsx +++ b/src/content-script/limit/modal/components/Footer.tsx @@ -5,15 +5,15 @@ import { TAG_NAME } from "@cs/limit/element" import { t } from "@cs/locale" import { Plus, Timer } from "@element-plus/icons-vue" import optionHolder from "@service/components/option-holder" -import { meetTimeLimit } from "@util/limit" +import { meetTimeLimit } from '@util/limit' import { ElButton } from "element-plus" import { computed, defineComponent } from "vue" import { useDelayHandler, useReason, useRule } from "../context" -async function handleMore5Minutes(rule: timer.limit.Item, callback: () => void) { +async function handleMore5Minutes(rule: timer.limit.Item | null, callback: () => void) { let promise: Promise | undefined = undefined const ele = document.querySelector(TAG_NAME)?.shadowRoot?.querySelector('body') - if (await judgeVerificationRequired(rule)) { + if (rule && await judgeVerificationRequired(rule)) { const option = await optionHolder.get() promise = processVerification(option, { appendTo: ele ?? undefined }) promise ? promise.then(callback).catch(() => { }) : callback() @@ -29,16 +29,16 @@ const _default = defineComponent(() => { const { type, allowDelay, delayCount = 0 } = reason.value || {} if (!allowDelay) return false - const { time, weekly, visit, waste, weeklyWaste } = rule.value || {} + const { time, weekly, visitTime, waste, weeklyWaste } = rule.value || {} let realLimit = 0, realWaste = 0 if (type === 'DAILY') { realLimit = time ?? 0 - realWaste = waste + realWaste = waste ?? 0 } else if (type === 'WEEKLY') { realLimit = weekly ?? 0 - realWaste = weeklyWaste + realWaste = weeklyWaste ?? 0 } else if (type === 'VISIT') { - realLimit = visit + realLimit = visitTime ?? 0 realWaste = reason.value?.getVisitTime?.() ?? 0 } else { return false diff --git a/src/content-script/limit/modal/context.ts b/src/content-script/limit/modal/context.ts index 3165ec73e..5af5803ad 100644 --- a/src/content-script/limit/modal/context.ts +++ b/src/content-script/limit/modal/context.ts @@ -10,11 +10,11 @@ const DELAY_HANDLER_KEY = 'delay_handler' export const provideReason = (app: App): Ref => { const reason = ref() - app?.provide(REASON_KEY, reason) + app.provide(REASON_KEY, reason) return reason } -export const useReason = () => inject(REASON_KEY) as Ref +export const useReason = () => inject(REASON_KEY) as Ref export const provideRule = () => { const reason = useReason() @@ -33,7 +33,7 @@ export const provideRule = () => { provide(RULE_KEY, rule) } -export const useRule = () => inject(RULE_KEY) as Ref +export const useRule = () => inject(RULE_KEY) as Ref export const provideDelayHandler = (app: App, handlers: () => void) => { app?.provide(DELAY_HANDLER_KEY, handlers) diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index c49db0008..7bb10547d 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -46,8 +46,13 @@ const createHeader = () => { } class ScreenLocker { + private beforeOverflow: string | undefined + private doLock() { - if (document?.documentElement) { + const ele = document?.documentElement + this.beforeOverflow = undefined + if (ele) { + this.beforeOverflow = ele.style.overflow document.documentElement.style.setProperty('overflow', 'hidden', 'important') } } @@ -60,7 +65,7 @@ class ScreenLocker { unlock() { if (document?.documentElement) { - document.documentElement.style.overflow = '' + document.documentElement.style.overflow = this.beforeOverflow ?? '' } } } @@ -78,6 +83,7 @@ class ModalInstance implements MaskModal { screenLocker = new ScreenLocker() constructor(url: string) { + (window as any)['__modal__'] = this this.url = url } @@ -115,8 +121,11 @@ class ModalInstance implements MaskModal { } private refresh() { - const reason = this.reasons?.[0] - reason ? this.show(reason) : this.hide() + setTimeout(() => { + // update vue ref in another micro task + const reason = this.reasons?.[0] + reason ? this.show(reason) : this.hide() + }) } private async init() { diff --git a/src/pages/app/components/About/index.tsx b/src/pages/app/components/About/index.tsx index 33a49dd95..f4ab649af 100644 --- a/src/pages/app/components/About/index.tsx +++ b/src/pages/app/components/About/index.tsx @@ -1,13 +1,12 @@ -import { defineComponent } from "vue" +import { type FunctionalComponent } from "vue" import ContentContainer from "../common/ContentContainer" import Description from "./Description" -const _default = defineComponent({ - render: () => ( - - - - ) -}) +const About: FunctionalComponent = () => ( + + + +) +About.displayName = 'About' -export default _default \ No newline at end of file +export default About \ No newline at end of file diff --git a/src/pages/app/components/HelpUs/index.tsx b/src/pages/app/components/HelpUs/index.tsx index d1a713a01..157546f1c 100644 --- a/src/pages/app/components/HelpUs/index.tsx +++ b/src/pages/app/components/HelpUs/index.tsx @@ -4,33 +4,32 @@ import { Pointer } from "@element-plus/icons-vue" import Box from "@pages/components/Box" import { CROWDIN_HOMEPAGE } from "@util/constant/url" import { ElAlert, ElButton, ElCard } from "element-plus" -import { defineComponent } from "vue" +import { type FunctionalComponent } from "vue" import ContentContainer from "../common/ContentContainer" import MemberList from "./MemberList" import ProgressList from "./ProgressList" const handleJump = () => createTabAfterCurrent(CROWDIN_HOMEPAGE) -const HelpUs = defineComponent(() => { - return () => ( - - - msg.helpUs.title)}> -
  • {t(msg => msg.helpUs.alert.l1)}
  • -
  • {t(msg => msg.helpUs.alert.l2)}
  • -
  • {t(msg => msg.helpUs.alert.l3)}
  • -
  • {t(msg => msg.helpUs.alert.l4)}
  • -
    - - - {t(msg => msg.helpUs.button)} - - - - -
    -
    - ) -}) +const HelpUs: FunctionalComponent = () => ( + + + msg.helpUs.title)}> +
  • {t(msg => msg.helpUs.alert.l1)}
  • +
  • {t(msg => msg.helpUs.alert.l2)}
  • +
  • {t(msg => msg.helpUs.alert.l3)}
  • +
  • {t(msg => msg.helpUs.alert.l4)}
  • +
    + + + {t(msg => msg.helpUs.button)} + + + + +
    +
    +) +HelpUs.displayName = 'HelpUs' export default HelpUs \ No newline at end of file diff --git a/src/pages/app/components/RuleMerge/index.tsx b/src/pages/app/components/RuleMerge/index.tsx index 287ddf5d1..1f018f847 100644 --- a/src/pages/app/components/RuleMerge/index.tsx +++ b/src/pages/app/components/RuleMerge/index.tsx @@ -7,22 +7,21 @@ import Flex from "@pages/components/Flex" import { ElCard } from "element-plus" -import { defineComponent } from "vue" +import { type FunctionalComponent } from "vue" import ContentContainer from "../common/ContentContainer" import AlertInfo from "./AlertInfo" import ItemList from "./ItemList" -const RuleMerge = defineComponent(() => { - return () => ( - - - - - - - - - ) -}) +const RuleMerge: FunctionalComponent = () => ( + + + + + + + + +) +RuleMerge.displayName = 'RuleMerge' export default RuleMerge diff --git a/src/pages/app/components/Whitelist/index.tsx b/src/pages/app/components/Whitelist/index.tsx index 8c0d96d54..f12c5bd39 100644 --- a/src/pages/app/components/Whitelist/index.tsx +++ b/src/pages/app/components/Whitelist/index.tsx @@ -8,28 +8,27 @@ import { t } from "@app/locale" import Flex from "@pages/components/Flex" import { ElAlert, ElCard } from "element-plus" -import { defineComponent } from "vue" +import { type FunctionalComponent } from "vue" import ContentContainer from "../common/ContentContainer" import WhitePanel from "./WhitePanel" -const Whitelist = defineComponent(() => { - return () => ( - - - - msg.whitelist.infoAlertTitle)} - style={{ padding: "15px 25px" }} - closable={false} - > -
  • {t(msg => msg.whitelist.infoAlert0)}
  • -
  • {t(msg => msg.whitelist.infoAlert1)}
  • -
    - -
    -
    -
    - ) -}) +const Whitelist: FunctionalComponent = () => ( + + + + msg.whitelist.infoAlertTitle)} + style={{ padding: "15px 25px" }} + closable={false} + > +
  • {t(msg => msg.whitelist.infoAlert0)}
  • +
  • {t(msg => msg.whitelist.infoAlert1)}
  • +
    + +
    +
    +
    +) +Whitelist.displayName = 'Whitelist' export default Whitelist diff --git a/test-e2e/limit/common.ts b/test-e2e/limit/common.ts index 5e29d8463..e8a34a321 100644 --- a/test-e2e/limit/common.ts +++ b/test-e2e/limit/common.ts @@ -25,9 +25,9 @@ export async function createLimitRule(rule: timer.limit.Rule, page: Page) { await sleep(.1) const { time, weekly, visitTime, count, weeklyCount } = rule || {} const timeInputs = await page.$$('.el-dialog .el-date-editor input') - await fillTimeLimit(time!, timeInputs[0], page) - await fillTimeLimit(weekly!, timeInputs[1], page) - await fillTimeLimit(visitTime!, timeInputs[2], page) + await fillTimeLimit(time, timeInputs[0], page) + await fillTimeLimit(weekly, timeInputs[1], page) + await fillTimeLimit(visitTime, timeInputs[2], page) const visitInputs = await page.$$('.el-dialog .el-input-number input') await fillVisitLimit(count!, visitInputs[0], page) await fillVisitLimit(weeklyCount!, visitInputs[1], page) @@ -35,9 +35,16 @@ export async function createLimitRule(rule: timer.limit.Rule, page: Page) { // 4. Save await sleep(.3) await page.click('.el-dialog .el-button.el-button--success') + if (rule.allowDelay) { + await page.waitForSelector('.el-table__body .el-table__row td:nth-child(9) .el-switch') + await page.evaluate(async () => { + document.querySelector('.el-table__body .el-table__row td:nth-child(9) .el-switch')?.click() + }) + await sleep(.3) + } } -export async function fillTimeLimit(value: number, input: ElementHandle, page: Page): Promise { +export async function fillTimeLimit(value: number | undefined, input: ElementHandle, page: Page): Promise { value = value ?? 0 const hour = Math.floor(value / 3600) value = value - hour * 3600 @@ -45,7 +52,7 @@ export async function fillTimeLimit(value: number, input: ElementHandle { const hourSpinner = el.querySelector('.el-scrollbar:first-child .el-scrollbar__wrap') hourSpinner!.scrollTo(0, hour * 32) @@ -58,6 +65,7 @@ export async function fillTimeLimit(value: number, input: ElementHandle, page: Page) { diff --git a/test-e2e/limit/visit-limit.test.ts b/test-e2e/limit/visit-limit.test.ts new file mode 100644 index 000000000..8585cce86 --- /dev/null +++ b/test-e2e/limit/visit-limit.test.ts @@ -0,0 +1,47 @@ +import { launchBrowser, LaunchContext, MOCK_URL, sleep } from '../common/base' +import { createLimitRule } from './common' + +describe('Time limit per visit', () => { + let context: LaunchContext + + beforeEach(async () => context = await launchBrowser()) + + afterEach(() => context.close()) + + test("More 5 minutes", async () => { + const limitPage = await context.openAppPage('/behavior/limit') + const demoRule: timer.limit.Rule = { + id: 1, name: 'TEST DAILY LIMIT', + cond: [MOCK_URL], + visitTime: 1, + enabled: true, allowDelay: true, locked: false, + } + + // 1. Insert limit rule + await createLimitRule(demoRule, limitPage) + + // 2. Open test page + const testPage = await context.newPageAndWaitCsInjected(MOCK_URL) + await sleep(2) + + // 3. Modal exist and then click more 5 minutes + const clicked = await testPage.evaluate(() => { + const shadow = document.querySelector('extension-time-tracker-overlay') + if (!shadow) return false + const button = shadow.shadowRoot?.querySelector('.el-button--primary') + button?.click() + return !!button + }) + expect(clicked).toBeTruthy() + + // 4. Modal disappear + await sleep(.5) + const modalExist = await testPage.evaluate(() => { + const shadow = document.querySelector('extension-time-tracker-overlay') + if (!shadow) return false + return !!shadow.shadowRoot!.querySelector('body:not([style*="display: none"])') + }) + expect(modalExist).toBeFalsy() + + }, 10000) +}) \ No newline at end of file From 962fbb4054d734c5d25f12a6d83386d7cc3fe99a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 23 Jul 2025 18:03:25 +0800 Subject: [PATCH 003/298] v3.5.4 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722e37e97..e7a31402f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.5.4] - 2025-07-23 + +- Fixed a bug again + ## [3.5.3] - 2025-07-14 - I have to update something, actually soluted some bugs diff --git a/package.json b/package.json index 6c3225c6c..1311eaa9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.3", + "version": "3.5.4", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From dfd258fdbbad245775d7ddedbb5b5552ad98060a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 25 Jul 2025 01:35:43 +0800 Subject: [PATCH 004/298] fix: fix style error of RTL layout (#510) --- package.json | 18 +++++++++--------- .../components/AnalysisFilter/TargetSelect.tsx | 1 + .../AnalysisFilter/select-v2.fix.sass | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/pages/app/components/Analysis/components/AnalysisFilter/select-v2.fix.sass diff --git a/package.json b/package.json index 1311eaa9c..98e316db7 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,15 @@ "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.0", "@crowdin/crowdin-api-client": "^1.46.0", - "@rsdoctor/rspack-plugin": "^1.1.8", - "@rspack/cli": "^1.4.8", - "@rspack/core": "^1.4.8", - "@swc/core": "^1.13.1", + "@rsdoctor/rspack-plugin": "^1.1.10", + "@rspack/cli": "^1.4.10", + "@rspack/core": "^1.4.10", + "@swc/core": "^1.13.2", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.1", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.0.15", + "@types/node": "^24.1.0", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.4.0", "babel-loader": "^10.0.0", @@ -47,13 +47,13 @@ "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", - "jest": "^30.0.4", - "jest-environment-jsdom": "^30.0.4", + "jest": "^30.0.5", + "jest-environment-jsdom": "^30.0.5", "jest-junit": "^16.0.0", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.14.0", + "puppeteer": "^24.15.0", "sass": "^1.89.2", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", @@ -75,7 +75,7 @@ "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", - "vue": "^3.5.17", + "vue": "^3.5.18", "vue-router": "^4.5.1" }, "engines": { diff --git a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx index 214d437e7..2b41871bb 100644 --- a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx +++ b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx @@ -12,6 +12,7 @@ import { computed, defineComponent, type FunctionalComponent, onMounted, ref, ty import { useAnalysisTarget } from "../../context" import type { AnalysisTarget } from "../../types" import { labelOfHostInfo } from "../../util" +import "./select-v2.fix.sass" const SITE_PREFIX = 'S' const CATE_PREFIX = 'C' diff --git a/src/pages/app/components/Analysis/components/AnalysisFilter/select-v2.fix.sass b/src/pages/app/components/Analysis/components/AnalysisFilter/select-v2.fix.sass new file mode 100644 index 000000000..e79e5e003 --- /dev/null +++ b/src/pages/app/components/Analysis/components/AnalysisFilter/select-v2.fix.sass @@ -0,0 +1,2 @@ +.el-vl__window.el-select-dropdown__list + direction: unset !important From 949a009ebac3885c40643dc0f4f49b92dc1969f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:54:47 +0800 Subject: [PATCH 005/298] i18n(download): download translations by bot (#515) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/analysis-resource.json | 6 +++++- src/i18n/message/app/limit-resource.json | 14 +++++++++++--- src/i18n/message/app/report-resource.json | 3 ++- src/i18n/message/app/site-manage-resource.json | 11 ++++++----- src/i18n/message/common/button-resource.json | 2 ++ src/i18n/message/common/item-resource.json | 2 ++ src/i18n/message/common/shared-resource.json | 1 + src/i18n/message/cs/console-resource.json | 4 ++++ 8 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json index 17d9c26a0..290c1ac24 100644 --- a/src/i18n/message/app/analysis-resource.json +++ b/src/i18n/message/app/analysis-resource.json @@ -298,6 +298,10 @@ } }, "ar": { + "target": { + "site": "الموقع الإلكتروني", + "cate": "الفئة" + }, "common": { "focusTotal": "إجمالي وقت التصفح", "visitTotal": "مجموع الزيارات", @@ -309,7 +313,7 @@ "summary": { "title": "ملخص", "day": "مجموع الأيام النشطة", - "firstDay": "الزيارة الأولي {value}", + "firstDay": "الزيارة الأولى {value}", "calendarTitle": "النشاط في الأسابيع الأخيرة" }, "trend": { diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index c5446cc55..55b40144e 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -358,6 +358,7 @@ "item": { "name": "Regelname", "condition": "Eingeschränkte URL", + "daily": "Tägliches Limit", "weekly": "Wöchentliches Limit", "weekStartInfo": "Der erste Tag jeder Woche ist {weekStart}, Sie können diesen Wert in den Statistikoptionen ändern", "delayCount": "Anzahl der Verspätungen", @@ -365,9 +366,13 @@ "visitTime": "Limit pro Besuch", "period": "Unzulässiger Zeitraum", "enabled": "Ermöglicht", + "locked": "Gesperrt", "effectiveDay": "Wirksam auf", "delayAllowed": "Weitere 5 Minuten", - "delayAllowedInfo": "Wenn es zu einer Zeitüberschreitung kommt, erlauben Sie eine vorübergehende Verzögerung von 5 Minuten" + "delayAllowedInfo": "Wenn es zu einer Zeitüberschreitung kommt, erlauben Sie eine vorübergehende Verzögerung von 5 Minuten", + "visits": "Besuche", + "or": "oder", + "notEffective": "Nicht wirksam" }, "step": { "base": "Basisdaten", @@ -381,10 +386,12 @@ "noUrl": "Nicht ausgefüllte eingeschränkte URL", "noRule": "Keine Regeln ausgefüllt", "deleteConfirm": "Möchten Sie die Regel [{name}] löschen?", + "lockConfirm": "Wenn gesperrt, müssen alle Operationen geprüft werden, auch wenn die Regel nicht ausgelöst wird.", "inputTestUrl": "Bitte geben Sie zunächst den zu testenden URL-Link ein", "clickTestButton": "Klicken Sie nach der Eingabe bitte auf die Schaltfläche ({buttonText}).", "noRuleMatched": "Die URL entspricht keinen Regeln", - "rulesMatched": "Die URL erfüllt die folgenden Regeln:" + "rulesMatched": "Die URL erfüllt die folgenden Regeln:", + "timeout": "Zeit ist abgelaufen! XD" }, "verification": { "inputTip": "Die Regel wurde ausgelöst oder gesperrt. Um fortzufahren, geben Sie bitte innerhalb von {second} Sekunden die Antwort auf die folgende Frage ein: {prompt}", @@ -395,7 +402,8 @@ "incorrectAnswer": "Falsche Antwort", "pi": "{digitCount} Ziffern von {startIndex} bis {endIndex} des Dezimalteils von π", "confession": "Zeit vergeht" - } + }, + "reminder": "Weniger als {min} Minuten bis zum Zeitlimit!" }, "fr": { "filterDisabled": "Activé uniquement", diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index 33e2ecd1b..188dbafcb 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -203,8 +203,9 @@ }, "ar": { "exportFileName": "وقت_التصفح_الخاص_بي", + "total": "إجمالي الزيارات: {visit} مرة، المدة الإجمالية: {focus}", "batchDelete": { - "noSelectedMsg": "الرجاء تحديد الصف الذي تريد حذفه في الجدول أولاً", + "noSelectedMsg": "الرجاء تحديد الصف الذي تريد حذفه من الجدول أولاً", "confirmMsg": "سيتم حذف {count} سجلًا لمواقع مثل {example} في {date}!", "confirmMsgAll": "سيتم حذف {count} سجلًا للمواقع مثل {example}!", "confirmMsgRange": "سيتم حذف {count} سجلًا لمواقع مثل {example} بين {start} و{end}!" diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index 45975c965..ef149967b 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -414,11 +414,12 @@ }, "ar": { "deleteConfirmMsg": "سيتم حذف {host}", + "genAliasConfirmMsg": "هل ترغب في إكمال أسماء المواقع تلقائيًا على دفعات؟", "column": { "type": "نوع الموقع", "alias": "اسم الموقع", "cate": "فئة الموقع", - "icon": "رمز" + "icon": "الأيقونة" }, "type": { "normal": { @@ -427,7 +428,7 @@ }, "merged": { "name": "اندمجت", - "info": "دمج إحصائيات أسماء النطافات المتعددة ذات الصلة، ويمكن تخصيص قواعد الدمج" + "info": "دمج إحصائيات أسماء النطاقات المرتبطة، مع إمكانية تخصيص قواعد الدمج" }, "virtual": { "name": "افتراضي", @@ -438,7 +439,7 @@ "name": "الاسم", "relatedMsg": "تم ربط هذه الفئة بمواقع {siteCount} ولا يمكن حذفها", "removeConfirm": "تأكيد حذف الفئة: {category}؟", - "batchChange": "تغيير التصنيفات", + "batchChange": "تغيير الفئات", "batchDisassociate": "فصل الفئات" }, "form": { @@ -449,8 +450,8 @@ "hostExistWarn": "{host} موجود فعلًا", "existedTag": "موجودة", "noSelected": "لم يتم تحديد الموقع", - "noSupported": "المواقع المحددة لا يمكن تعيين الفئات", - "disassociatedMsg": "هل تريد مسح الفئات من جميع المواقع المحددة؟", + "noSupported": "لا يمكن تعيين فئات للمواقع المحددة", + "disassociatedMsg": "هل تريد إخلاء فئات جميع المواقع المحددة؟", "batchDeleteMsg": "هل تريد حذف جميع المواقع المحددة؟" } } diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json index 61da58e0c..0a01dc489 100644 --- a/src/i18n/message/common/button-resource.json +++ b/src/i18n/message/common/button-resource.json @@ -209,6 +209,7 @@ }, "ar": { "create": "جديد", + "add": "أضف", "delete": "حذف", "batchDelete": "حذف جماعي", "modify": "تعديل", @@ -224,6 +225,7 @@ "operation": "إجراءات", "configuration": "إعدادات", "clear": "مسح", + "enable": "تفعيل", "batchEnable": "تمكين جماعي", "batchDisable": "تعطيل جماعي" } diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index 0e89dc3a2..c3047d5d7 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -170,7 +170,9 @@ "ar": { "date": "التاريخ", "host": "الموقع", + "group": "مجموعة علامات التبويب", "focus": "المدة", + "run": "تشغيل الوقت", "time": "الزيارات", "operation": { "add2Whitelist": "القائمة البيضاء", diff --git a/src/i18n/message/common/shared-resource.json b/src/i18n/message/common/shared-resource.json index 9717c5108..85a91b6c9 100644 --- a/src/i18n/message/common/shared-resource.json +++ b/src/i18n/message/common/shared-resource.json @@ -148,6 +148,7 @@ "date": "تاريخ", "domain": "القيمة", "cate": "الفئة", + "group": "مجموعة علامات التبويب", "notMerge": "لا دمج" } }, diff --git a/src/i18n/message/cs/console-resource.json b/src/i18n/message/cs/console-resource.json index 43fc15267..f32de0deb 100644 --- a/src/i18n/message/cs/console-resource.json +++ b/src/i18n/message/cs/console-resource.json @@ -38,5 +38,9 @@ "ru": { "consoleLog": "Сегодня вы открывали {host} {time} раз(а), потратив {focus} на просмотр.", "closeAlert": "Вы можете отключить эти уведомления в настройках [{appName}]!" + }, + "ar": { + "consoleLog": "اليوم فتحت {host} {time} مرة, أنفقت {focus} أثناء تصفحها.", + "closeAlert": "يمكنك تعطيل هذه الإشعارات في إعدادات [{appName}]" } } \ No newline at end of file From 3291890a7cc0f32b65a30dd95bb6ffb1e6d90f7c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 26 Jul 2025 00:33:57 +0800 Subject: [PATCH 006/298] fix: can't scroll the page afte limit modal closed(#513) --- src/content-script/limit/modal/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index 7bb10547d..5ec1085e9 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -46,13 +46,9 @@ const createHeader = () => { } class ScreenLocker { - private beforeOverflow: string | undefined - private doLock() { const ele = document?.documentElement - this.beforeOverflow = undefined if (ele) { - this.beforeOverflow = ele.style.overflow document.documentElement.style.setProperty('overflow', 'hidden', 'important') } } @@ -65,7 +61,7 @@ class ScreenLocker { unlock() { if (document?.documentElement) { - document.documentElement.style.overflow = this.beforeOverflow ?? '' + document.documentElement.style.overflow = 'auto' } } } From 8ecc750cafe2167655d04b9cca3c66cca05ab82d Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 26 Jul 2025 00:57:05 +0800 Subject: [PATCH 007/298] v3.5.5 --- CHANGELOG.md | 8 +++++++- package.json | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a31402f..d6717e08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,19 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.5.5] - 2025-0-26 + +- Still that bug = = +- Added some translations, thanks to [zakno52](https://github.com/zakno52) +- Fixed some styling errors in RTL layout + ## [3.5.4] - 2025-07-23 - Fixed a bug again ## [3.5.3] - 2025-07-14 -- I have to update something, actually soluted some bugs +- I had to update something, actually fixed some bugs ## [3.5.2] - 2025-06-24 diff --git a/package.json b/package.json index 98e316db7..556b28117 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.4", + "version": "3.5.5", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -81,4 +81,4 @@ "engines": { "node": ">=20" } -} +} \ No newline at end of file From 105d54a8a0b632c310ffc6aef03ed56d0857e8c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 00:58:18 +0800 Subject: [PATCH 008/298] i18n(download): download translations by bot (#516) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/limit-resource.json | 16 +++++++++++++--- src/i18n/message/app/report-resource.json | 1 + src/i18n/message/app/site-manage-resource.json | 1 + src/i18n/message/common/button-resource.json | 2 ++ src/i18n/message/common/item-resource.json | 2 ++ src/i18n/message/common/shared-resource.json | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 55b40144e..fd72317bf 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -407,18 +407,25 @@ }, "fr": { "filterDisabled": "Activé uniquement", + "wildcardTip": "Vous pouvez utiliser des wildcards pour correspondre à des sous-domaines ou sous-pages !", "item": { "name": "Nom de règle", "condition": "URL restreinte", + "daily": "Limite quotidienne", + "weekly": "Limite hebdomadaire", "weekStartInfo": "Le premier jour de chaque semaine est {weekStart}, vous pouvez modifier cette valeur dans les options de statistiques", "delayCount": "Nombre de retards", "detail": "Détail des règles", "visitTime": "Limite par visite", "period": "Périodes bloquées", "enabled": "Activé", + "locked": "Verrouillé", "effectiveDay": "Effectif le", "delayAllowed": "Plus de 5 minutes", - "delayAllowedInfo": "Si le délai est écoulé, autorisez un délai temporaire de 5 minutes" + "delayAllowedInfo": "Si le délai est écoulé, autorisez un délai temporaire de 5 minutes", + "visits": "visites", + "or": "ou", + "notEffective": "Non efficace" }, "step": { "base": "Informations de base", @@ -432,10 +439,12 @@ "noUrl": "Aucune URL de restriction configurée", "noRule": "Aucune règle remplie", "deleteConfirm": "Voulez-vous supprimer la règle [{name}]?", + "lockConfirm": "Si verrouillé, toutes les opérations nécessiteront une vérification, même si la règle n'est pas activée.", "inputTestUrl": "Veuillez entrer le lien URL à tester en premier", "clickTestButton": "Après l'entrée, cliquez sur le bouton ({buttonText})", "noRuleMatched": "L'URL ne correspond à aucune règle", - "rulesMatched": "L'URL atteint les règles suivantes :" + "rulesMatched": "L'URL atteint les règles suivantes :", + "timeout": "Temps écoulé ! XD" }, "verification": { "inputTip": "La règle a été déclenchée ou verrouillée. Pour continuer, veuillez répondre à la question suivante dans un délai de {second} secondes : {prompt}", @@ -446,7 +455,8 @@ "incorrectAnswer": "Réponse incorrecte", "pi": "{digitCount} chiffres de {startIndex} à {endIndex} de la partie décimale de π", "confession": "Le temps, c'est de l'argent" - } + }, + "reminder": "Moins de {min} minutes jusqu'à la limite de temps !" }, "ru": { "filterDisabled": "Только включен", diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index 188dbafcb..c79948645 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -163,6 +163,7 @@ }, "fr": { "exportFileName": "Mon_heure_de_navigation", + "total": "Visites totales : {visit} fois, durée totale : {focus}", "batchDelete": { "noSelectedMsg": "Veuillez d'abord sélectionner la ligne que vous souhaitez supprimer dans le tableau", "confirmMsg": "{count} enregistrements pour des sites comme {example} le {date} seront supprimés !", diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index ef149967b..c267bd492 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -342,6 +342,7 @@ }, "fr": { "deleteConfirmMsg": "{host} sera supprimé", + "genAliasConfirmMsg": "Compléter automatiquement les noms de sites par lots ?", "column": { "type": "Type de Site", "alias": "Nom du site", diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json index 0a01dc489..37b40a69f 100644 --- a/src/i18n/message/common/button-resource.json +++ b/src/i18n/message/common/button-resource.json @@ -169,6 +169,7 @@ }, "fr": { "create": "Nouveau", + "add": "Ajouter", "delete": "Supprimer", "batchDelete": "Suppression par Lot", "modify": "Modifier", @@ -184,6 +185,7 @@ "operation": "Opérations", "configuration": "Paramètres", "clear": "Effacer", + "enable": "Activer", "batchEnable": "Activation par Lot", "batchDisable": "Désactivation par Lot" }, diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index c3047d5d7..962828924 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -138,7 +138,9 @@ "fr": { "date": "Date", "host": "Site", + "group": "Groupe d'onglets", "focus": "Durée", + "run": "Durée d'exécution", "time": "Visites", "operation": { "add2Whitelist": "Whitelist", diff --git a/src/i18n/message/common/shared-resource.json b/src/i18n/message/common/shared-resource.json index 85a91b6c9..281a79740 100644 --- a/src/i18n/message/common/shared-resource.json +++ b/src/i18n/message/common/shared-resource.json @@ -120,6 +120,7 @@ "date": "Date", "domain": "URL", "cate": "Catégorie", + "group": "Groupe d'onglets", "notMerge": "Non Fusionner" } }, From ef131e74ba457f3566f8de6a77b9e246a1e21aa7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:08:24 +0800 Subject: [PATCH 009/298] i18n(download): download translations by bot (#517) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/option-resource.json | 18 ++++++++++++++++-- src/i18n/message/common/item-resource.json | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index c040c95aa..372cd0ecb 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -1090,7 +1090,8 @@ "off": "Toujours éteint", "timed": "Horaire" } - } + }, + "animationDuration": "La durée de l'animation initiale du graphique {input}" }, "statistics": { "title": "Statistiques", @@ -1099,6 +1100,9 @@ "countLocalFiles": "{input} S'il faut compter le temps jusqu'à {localFileTime} {info} dans le navigateur", "localFileTime": "lire un fichier local", "localFilesInfo": "Prend en charge les fichiers de types tels que PDF, image, txt et json.", + "countTabGroup": "{input} Suivre le temps des groupes d'onglets {info}", + "tabGroupInfo": "Lorsque vous supprimez un groupe d'onglets, les données seront également supprimées.", + "tabGroupsPermGrant": "Cette fonctionnalité nécessite des autorisations pertinentes", "collectSiteName": "{input} S'il faut collecter automatiquement {siteName} {siteNameUsage} lors de la visite de la page d'accueil du site", "siteName": "le nom du site", "siteNameUsage": "Les données sont uniquement stockées localement et seront affichées à la place de l'URL pour augmenter la reconnaissance. Bien entendu, vous pouvez également personnaliser le nom de chaque site.", @@ -1109,6 +1113,7 @@ }, "dailyLimit": { "prompt": "Invite affichée en cas de restriction {input}", + "reminder": "{input} Rappel {minInput} minutes avant la fin du temps", "level": { "label": "Comment déverrouiller en étant restreint {input}", "nothing": "Autoriser le déverrouillage direct sur la page d'administration", @@ -1123,7 +1128,9 @@ }, "strict": "Ne pas autoriser le déblocage quand même", "strictTitle": "Confirmation de l'opération", - "strictContent": "Lorsque vous sélectionnez cette option, si un site déclenche une limite quotidienne, vous ne serez pas autorisé à le débloquer manuellement sauf en attendant le lendemain. Si les règles ne sont pas correctement configurées, elles peuvent très bien gêner vos routines !" + "strictContent": "Lorsque vous sélectionnez cette option, si un site déclenche une limite quotidienne, vous ne serez pas autorisé à le débloquer manuellement sauf en attendant le lendemain. Si les règles ne sont pas correctement configurées, elles peuvent très bien gêner vos routines !", + "pswFormLabel": "Mot de passe", + "pswFormAgain": "Retapez" } }, "backup": { @@ -1176,6 +1183,13 @@ }, "resetButton": "Réinitialiser", "resetSuccess": "Remise à zéro avec succès !", + "exportButton": "Exporter les paramètres", + "importButton": "Importer les paramètres", + "exportSuccess": "Paramètres exportés avec succès", + "importSuccess": "Paramètres importés avec succès", + "importError": "Échec de l'import : fichier de paramètres invalide", + "importConfirm": "Paramètres importés avec succès, veuillez Actualiser la page pour appliquer les modifications !", + "reloadButton": "Actualiser", "defaultValue": "Par défaut : {default}" }, "ru": { diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index 962828924..6a516a3e2 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -174,7 +174,7 @@ "host": "الموقع", "group": "مجموعة علامات التبويب", "focus": "المدة", - "run": "تشغيل الوقت", + "run": "وقت التشغيل", "time": "الزيارات", "operation": { "add2Whitelist": "القائمة البيضاء", From f496cd402d459a379088da357857856062ec2f8a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:03:50 +0800 Subject: [PATCH 010/298] i18n(download): download translations by bot (#518) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/limit-resource.json | 16 +++++++++++++--- src/i18n/message/app/option-resource.json | 19 +++++++++++++++++++ src/i18n/message/common/item-resource.json | 1 + src/i18n/message/common/shared-resource.json | 1 + 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index fd72317bf..9c9057966 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -504,18 +504,25 @@ }, "ar": { "filterDisabled": "تم التمكين فقط", + "wildcardTip": "يمكنك استخدام الرموز البديلة لتشمل النطاقات أو الصفحات الفرعية!", "item": { "name": "اسم القاعدة", "condition": "عنوان URL مقيد", + "daily": "الحد اليومي", + "weekly": "الحد الأسبوعي", "weekStartInfo": "اليوم الأول من كل أسبوع هو: {weekStart}، يمكنك تغيير هذه القيمة في خيارات الإحصائيات", "delayCount": "عدد التأخير", "detail": "تفاصيل القاعدة", "visitTime": "الحد الزمني لكل زيارة", "period": "فترات محظورة", "enabled": "مُمَكَّن", + "locked": "مقفل", "effectiveDay": "ساري المفعول على", "delayAllowed": "5 دقائق إضافية", - "delayAllowedInfo": "إذا انتهت المهلة، اسمح بتأخير مؤقت لمدة 5 دقائق" + "delayAllowedInfo": "إذا انتهت المهلة، اسمح بتأخير مؤقت لمدة 5 دقائق", + "visits": "الزيارات", + "or": "أو", + "notEffective": "غير فعال" }, "step": { "base": "معلومات أساسية", @@ -529,10 +536,12 @@ "noUrl": "لم يتم تكوين عناوين URL الخاصة بالقيود", "noRule": "لم يتم ملء أي قواعد", "deleteConfirm": "هل تريد حذف القاعدة [{name}]؟", + "lockConfirm": "إذا تم القَفل، فإن جميع العمليات ستتطلب التحقق حتى وإن لم يتم تفعيل القاعدة.", "inputTestUrl": "الرجاء إدخال رابط URL ليتم اختباره أولاً", "clickTestButton": "بعد الإدخال، الرجاء الضغط على الزر ({buttonText})", "noRuleMatched": "عنوان URL لا يصطدم بأي قواعد", - "rulesMatched": "يتوافق عنوان URL مع القواعد التالية:" + "rulesMatched": "يتوافق عنوان URL مع القواعد التالية:", + "timeout": "انتهى الوقت! XD" }, "verification": { "inputTip": "تم تفعيل القاعدة أو قفلها. للمتابعة، يُرجى إدخال إجابة السؤال التالي خلال {second} ثانية: {prompt}", @@ -543,6 +552,7 @@ "incorrectAnswer": "إجابة خاطئة", "pi": "{digitCount} أرقام من {startIndex} إلى {endIndex} من الجزء العشري من π", "confession": "الوقت سريع الزوال" - } + }, + "reminder": "أقل من {min} دقيقة على انتهاء الوقت المحدد!" } } \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index 372cd0ecb..2a554d704 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -184,6 +184,9 @@ "countLocalFiles": "{input} 是否統計瀏覽器 {localFileTime} {info}", "localFileTime": "讀取本機檔案時間", "localFilesInfo": "支援 PDF、圖片、txt 及 json 等格式", + "countTabGroup": "{input} 是否追蹤分頁群組的時間 {info}", + "tabGroupInfo": "刪除分頁群組時,相關的時間追蹤數據也會一併清除。", + "tabGroupsPermGrant": "此功能需要相關權限才能運作", "collectSiteName": "{input} 造訪網站首頁時自動收集 {siteName} {siteNameUsage}", "siteName": "網站名稱", "siteNameUsage": "資料僅儲存於本地,將取代網域名稱提升辨識度,您可自訂各網站名稱", @@ -1238,5 +1241,21 @@ "resetButton": "Сброс", "resetSuccess": "Сброс к настройкам по умолчанию выполнен успешно!", "defaultValue": "По умолчанию: {default}" + }, + "ar": { + "yes": "نعم", + "no": "لا", + "popup": { + "title": "صفحة منبثقة", + "max": "عرض أول {input} عنصر من البيانات", + "displaySiteName": "{input} هل يتم عرض {siteName} بدلاً من الرابط" + }, + "appearance": { + "title": "المظهر", + "displayWhitelist": "{input} هل يتم عرض {whitelist} في {contextMenu}" + }, + "resetButton": "إعادة الضبط", + "resetSuccess": "تم إعادة الضبط إلى الإعدادات الإفتراضية بنجاح!", + "defaultValue": "الوضع الإفتراضي: {default}" } } \ No newline at end of file diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index 6a516a3e2..aeed037a0 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -20,6 +20,7 @@ "zh_TW": { "date": "日期", "host": "網域", + "group": "分頁群組", "focus": "瀏覽時長", "run": "執行時間", "time": "造訪次數", diff --git a/src/i18n/message/common/shared-resource.json b/src/i18n/message/common/shared-resource.json index 281a79740..f14e4a6a7 100644 --- a/src/i18n/message/common/shared-resource.json +++ b/src/i18n/message/common/shared-resource.json @@ -50,6 +50,7 @@ "date": "依日期", "domain": "依網域", "cate": "依類別", + "group": "分頁群組", "notMerge": "不合並" } }, From 93369345bc8c52cfe9b60b58f0e8587571d622e1 Mon Sep 17 00:00:00 2001 From: sheepie Date: Mon, 28 Jul 2025 10:09:42 +0800 Subject: [PATCH 011/298] chore: remove unavailable codebeat badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4ba2747c2..da81bed88 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Time Tracker for Browser [![codecov](https://codecov.io/gh/sheepzh/timer/branch/main/graph/badge.svg?token=S98QSBSKCR&style=flat-square)](https://codecov.io/gh/sheepzh/timer) -[![codebeat badge](https://codebeat.co/badges/b26df573-8143-4a53-8617-1e1eb4fe7b86)](https://codebeat.co/projects/github-com-sheepzh-timer-main) [![](https://img.shields.io/badge/license-Anti%20996-blue)](https://github.com/996icu/996.ICU) [![](https://img.shields.io/github/v/release/sheepzh/timer)](https://github.com/sheepzh/timer/releases) [![Crowdin](https://badges.crowdin.net/timer-chrome-edge-firefox/localized.svg)](https://crowdin.com/project/timer-chrome-edge-firefox) From 6702fa735b3a100734615a8347062ab8616a4f59 Mon Sep 17 00:00:00 2001 From: sheepie Date: Mon, 28 Jul 2025 10:10:19 +0800 Subject: [PATCH 012/298] chore: remove unavailable codebeat badge --- README-zh.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README-zh.md b/README-zh.md index 4b6d608d7..498162e6b 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,8 +1,6 @@ # 网费很贵 [![codecov](https://codecov.io/gh/sheepzh/timer/branch/main/graph/badge.svg?token=S98QSBSKCR&style=flat-square)](https://codecov.io/gh/sheepzh/timer) -[![codebeat badge](https://codebeat.co/badges/69a88b51-2a07-4944-98dc-603a99d8a9f9)](https://codebeat.co/projects/github-com-sheepzh-timer-main) -[![](https://www.travis-ci.com/sheepzh/timer.svg?branch=main)](https://www.travis-ci.com/sheepzh/timer.svg?branch=main) [![](https://img.shields.io/badge/license-Anti%20996-blue)](https://github.com/996icu/996.ICU) [![](https://img.shields.io/github/v/release/sheepzh/timer)](https://github.com/sheepzh/timer/releases) From 9ff55b60931f2a3825defe059f13ae06bd3a0ee8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:57:11 +0800 Subject: [PATCH 013/298] i18n(download): download translations by bot (#519) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/help-us-resource.json | 12 +++++++ src/i18n/message/app/option-resource.json | 40 +++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/i18n/message/app/help-us-resource.json b/src/i18n/message/app/help-us-resource.json index f2243de65..68956ce56 100644 --- a/src/i18n/message/app/help-us-resource.json +++ b/src/i18n/message/app/help-us-resource.json @@ -118,5 +118,17 @@ "button": "Перейти в Crowdin", "loading": "Проверка перевода...", "contributors": "Список участников" + }, + "ar": { + "title": "ساهم في تحسين ترجمة هذه الإضافة!", + "alert": { + "l1": "نظرًا لقدرات المطور اللغوية، يدعم هذا الامتداد حاليا اللغة الصينية والإنجليزية فقط. اللغات الأخرى إما غير مدعومة أو تعتمد بشكل كبير على الترجمة الآلية.", + "l2": "لتوفير تجربة مستخدم أفضل، نقوم باستضافة مهام الترجمة للغات الأخرى على منصة Crowdin، نظام إدارة ترجمة مجاني للبرمجيات المفتوحة المصدر.", + "l3": "إذا وجدت هذه الإضافة مفيدة لك وترغب في تحسين ترجمتها، يمكنك النقر على الزر أدناه للذهاب إلى صفحة المشروع على Crowdin.", + "l4": "عند وصول تقدم ترجمة لغة معينة إلى 50%، سنقوم بدعمها رسميًا في هذا الامتداد." + }, + "button": "الذهاب إلى Crowdin", + "loading": "التحقق من تقدم الترجمة...", + "contributors": "قائمة المساهمين" } } \ No newline at end of file diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index 2a554d704..da80cedc5 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -1252,7 +1252,45 @@ }, "appearance": { "title": "المظهر", - "displayWhitelist": "{input} هل يتم عرض {whitelist} في {contextMenu}" + "displayWhitelist": "{input} هل يتم عرض {whitelist} في {contextMenu}", + "displayBadgeText": "{input} هل يتم عرض {timeInfo} على {icon}", + "badgeBgColor": "لون خلفية المؤقت المتواجد على الأيقونة {input}", + "icon": "أيقونة الإضافة", + "badgeTextContent": "وقت التصفح للموقع الحالي", + "locale": { + "label": "اللغة {input}", + "default": "لغة المتصفح", + "changeConfirm": "تم تغيير اللغة بنجاح، يرجى إعادة تحميل هذه الصفحة!", + "reloadButton": "إعادة التحميل" + }, + "printInConsole": { + "label": "{input} هل يتم طباعة {info} في {console}", + "console": "وحدة التحكم", + "info": "عدد زيارات الموقع الحالي اليوم" + }, + "darkMode": { + "label": "الوضع الداكن {input}", + "options": { + "default": "نفس وضع المتصفح", + "on": "تفعيل دائم", + "off": "إيقاف دائم", + "timed": "تفعيل مؤقت" + } + }, + "animationDuration": "مدة تحريك الرسوم البيانية {input}" + }, + "statistics": { + "title": "الإحصائيات", + "countLocalFiles": "{input} هل يتم حساب الوقت عند فتح {localFileTime} في المتصفح {info}", + "localFileTime": "الملفات المحلية", + "localFilesInfo": "يدعم ملفات مثل PDF، الصور، txt و json.", + "collectSiteName": "{input} هل يتم أخذ {siteName} {siteNameUsage} تلقائيًا عند زيارة الصفحة الرئيسية", + "siteName": "اسم الموقع الإلكتروني", + "siteNameUsage": "يتم تخزين البيانات محليًا فقط وسيتم عرضها بدلاً من الرابط لزيادة الوضوح. يمكنك أيضًا تخصيص اسم كل موقع.", + "fileAccessDisabled": "الوصول إلى عناوين الملفات غير مسموح به حاليًا. يرجى تفعيل الخاصية من صفحة الإدارة أولاً" + }, + "backup": { + "title": "النسخ الاحتياطي للبيانات" }, "resetButton": "إعادة الضبط", "resetSuccess": "تم إعادة الضبط إلى الإعدادات الإفتراضية بنجاح!", From a638e450d51c2f6af1ecae2fae6774c60a1777fe Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 29 Jul 2025 15:45:12 +0800 Subject: [PATCH 014/298] fix: missing translators --- package.json | 4 +- src/api/crowdin.ts | 25 ++++++++---- src/pages/app/components/About/index.tsx | 11 +++-- src/pages/app/components/DataManage/index.tsx | 29 ++++++++------ src/pages/app/components/HelpUs/index.tsx | 40 ++++++++++--------- src/pages/app/components/Option/index.tsx | 13 ++++-- 6 files changed, 73 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 556b28117..2aa30b4a2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@rsdoctor/rspack-plugin": "^1.1.10", "@rspack/cli": "^1.4.10", "@rspack/core": "^1.4.10", - "@swc/core": "^1.13.2", + "@swc/core": "^1.13.3", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.1", "@types/decompress": "^4.2.7", @@ -68,7 +68,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", - "@vueuse/core": "^13.5.0", + "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", "echarts": "^5.6.0", "element-plus": "2.10.4", diff --git a/src/api/crowdin.ts b/src/api/crowdin.ts index 02f2f25f3..127774e88 100644 --- a/src/api/crowdin.ts +++ b/src/api/crowdin.ts @@ -39,12 +39,23 @@ export async function getTranslationStatus(): Promise { } export async function getMembers(): Promise { - const limit = 20 + const result: MemberInfo[] = [] const auth = `Bearer ${PUBLIC_TOKEN}` - const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/members?limit=${limit}` - const response: AxiosResponse = await axios.get(url, { - headers: { "Authorization": auth } - }) - const data: { data: { data: MemberInfo }[] } = response.data - return data.data.map(i => i.data) + + const limit = 10 + let offset = 0 + while (true) { + const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/members?limit=${limit}&offset=${offset}` + const response: AxiosResponse = await axios.get(url, { + headers: { "Authorization": auth } + }) + const data: { data: { data: MemberInfo }[] } = response.data + const newItems = data?.data?.map(i => i.data) ?? [] + result.push(...newItems) + + if (newItems.length < limit) break + + offset += limit + } + return result } \ No newline at end of file diff --git a/src/pages/app/components/About/index.tsx b/src/pages/app/components/About/index.tsx index f4ab649af..084769c31 100644 --- a/src/pages/app/components/About/index.tsx +++ b/src/pages/app/components/About/index.tsx @@ -1,11 +1,14 @@ -import { type FunctionalComponent } from "vue" +import { ElScrollbar } from 'element-plus' +import { type FunctionalComponent, type StyleValue } from "vue" import ContentContainer from "../common/ContentContainer" import Description from "./Description" const About: FunctionalComponent = () => ( - - - + + + + + ) About.displayName = 'About' diff --git a/src/pages/app/components/DataManage/index.tsx b/src/pages/app/components/DataManage/index.tsx index dbeac481c..093ebc43c 100644 --- a/src/pages/app/components/DataManage/index.tsx +++ b/src/pages/app/components/DataManage/index.tsx @@ -6,7 +6,8 @@ */ import Flex from "@pages/components/Flex" -import { defineComponent } from "vue" +import { ElScrollbar } from 'element-plus' +import { defineComponent, type StyleValue } from "vue" import ContentContainer from "../common/ContentContainer" import ClearPanel from "./ClearPanel" import MemoryInfo from "./MemoryInfo" @@ -17,18 +18,20 @@ export default defineComponent(() => { initDataManage() return () => ( - - - - + + + + + + + + + + + + - - - - - - - - + + ) }) \ No newline at end of file diff --git a/src/pages/app/components/HelpUs/index.tsx b/src/pages/app/components/HelpUs/index.tsx index 157546f1c..0e4a50d5a 100644 --- a/src/pages/app/components/HelpUs/index.tsx +++ b/src/pages/app/components/HelpUs/index.tsx @@ -3,8 +3,8 @@ import { t } from "@app/locale" import { Pointer } from "@element-plus/icons-vue" import Box from "@pages/components/Box" import { CROWDIN_HOMEPAGE } from "@util/constant/url" -import { ElAlert, ElButton, ElCard } from "element-plus" -import { type FunctionalComponent } from "vue" +import { ElAlert, ElButton, ElCard, ElScrollbar } from "element-plus" +import { type FunctionalComponent, type StyleValue } from "vue" import ContentContainer from "../common/ContentContainer" import MemberList from "./MemberList" import ProgressList from "./ProgressList" @@ -12,23 +12,25 @@ import ProgressList from "./ProgressList" const handleJump = () => createTabAfterCurrent(CROWDIN_HOMEPAGE) const HelpUs: FunctionalComponent = () => ( - - - msg.helpUs.title)}> -
  • {t(msg => msg.helpUs.alert.l1)}
  • -
  • {t(msg => msg.helpUs.alert.l2)}
  • -
  • {t(msg => msg.helpUs.alert.l3)}
  • -
  • {t(msg => msg.helpUs.alert.l4)}
  • -
    - - - {t(msg => msg.helpUs.button)} - - - - -
    -
    + + + + msg.helpUs.title)}> +
  • {t(msg => msg.helpUs.alert.l1)}
  • +
  • {t(msg => msg.helpUs.alert.l2)}
  • +
  • {t(msg => msg.helpUs.alert.l3)}
  • +
  • {t(msg => msg.helpUs.alert.l4)}
  • +
    + + + {t(msg => msg.helpUs.button)} + + + + +
    +
    +
    ) HelpUs.displayName = 'HelpUs' diff --git a/src/pages/app/components/Option/index.tsx b/src/pages/app/components/Option/index.tsx index acb04382b..d1732c994 100644 --- a/src/pages/app/components/Option/index.tsx +++ b/src/pages/app/components/Option/index.tsx @@ -5,7 +5,8 @@ * https://opensource.org/licenses/MIT */ import { MediaSize, useMediaSize } from "@hooks/useMediaSize" -import { defineComponent, ref, type Ref } from "vue" +import { ElScrollbar } from 'element-plus' +import { defineComponent, ref, type Ref, type StyleValue } from "vue" import { type JSX } from "vue/jsx-runtime" import { type OptionCategory, type OptionInstance } from "./common" import AccessibilityOption from "./components/AccessibilityOption" @@ -41,9 +42,13 @@ const _default = defineComponent(() => { const handleReset = (cate: OptionCategory) => paneRefMap[cate]?.value?.reset?.() - return () => mediaSize.value <= MediaSize.sm - ? + : } + + ) }) export default _default \ No newline at end of file From 5dcdfc30d27b162d14f74cec7e3f6b8c3b5588f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:59:52 +0800 Subject: [PATCH 015/298] build(deps): bump echarts from 5.6.0 to 6.0.0 (#521) Bumps [echarts](https://github.com/apache/echarts) from 5.6.0 to 6.0.0. - [Release notes](https://github.com/apache/echarts/releases) - [Commits](https://github.com/apache/echarts/compare/5.6.0...6.0.0) --- updated-dependencies: - dependency-name: echarts dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2aa30b4a2..1753728c0 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@element-plus/icons-vue": "^2.3.1", "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", - "echarts": "^5.6.0", + "echarts": "^6.0.0", "element-plus": "2.10.4", "js-base64": "^3.7.7", "punycode": "^2.3.1", From ea83c30ef56b834a56ae47b7d7b5329a2a8b3cb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:58:33 +0800 Subject: [PATCH 016/298] chore(psl): update PSL list by bot (#523) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/util/psl/rules.json | 329 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 323 insertions(+), 6 deletions(-) diff --git a/src/util/psl/rules.json b/src/util/psl/rules.json index f728ef2f3..601f53368 100644 --- a/src/util/psl/rules.json +++ b/src/util/psl/rules.json @@ -571,48 +571,315 @@ "c": { "on": { "c": { + "af-south-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-east-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, "ap-northeast-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-northeast-2": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-northeast-3": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-south-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-south-2": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "ap-southeast-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "ap-southeast-2": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-southeast-3": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-southeast-4": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ap-southeast-5": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ca-central-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "ca-west-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "eu-central-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "eu-central-2": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "eu-north-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "eu-south-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "eu-south-2": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "eu-west-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "eu-west-2": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "eu-west-3": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "il-central-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "me-central-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "me-south-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "sa-east-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "us-east-1": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "us-east-2": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "us-gov-east-1": { + "c": { + "transfer-webapp": 1, + "transfer-webapp-fips": 1 + } + }, + "us-gov-west-1": { + "c": { + "transfer-webapp": 1, + "transfer-webapp-fips": 1 + } + }, + "us-west-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } }, "us-west-2": { "c": { + "airflow": { + "c": { + "*": 1 + } + }, "transfer-webapp": 1 } } @@ -1990,6 +2257,34 @@ } } }, + "amazonwebservices": { + "c": { + "on": { + "c": { + "cn-north-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + }, + "cn-northwest-1": { + "c": { + "airflow": { + "c": { + "*": 1 + } + }, + "transfer-webapp": 1 + } + } + } + } + } + }, "sagemaker": { "c": { "cn-north-1": { @@ -2235,6 +2530,11 @@ "*": 1 } }, + "ap-southeast-5": { + "c": { + "*": 1 + } + }, "ca-central-1": { "c": { "*": 1 @@ -3307,6 +3607,11 @@ "auth": 1 } }, + "ap-southeast-7": { + "c": { + "auth": 1 + } + }, "ca-central-1": { "c": { "auth": 1 @@ -3372,6 +3677,11 @@ "auth": 1 } }, + "mx-central-1": { + "c": { + "auth": 1 + } + }, "sa-east-1": { "c": { "auth": 1 @@ -3897,6 +4207,11 @@ } } }, + "oaiusercontent": { + "c": { + "*": 1 + } + }, "observableusercontent": { "c": { "static": 1 @@ -4405,11 +4720,6 @@ "taifun-dns": 1, "test-iserv": 1, "traeumtgerade": 1, - "uberspace": { - "c": { - "*": 1 - } - }, "virtual-user": 1, "virtualuser": 1, "webspaceconfig": 1, @@ -11095,7 +11405,12 @@ "us": 1, "webhop": 1, "webredirect": 1, - "wmcloud": 1, + "wmcloud": { + "c": { + "beta": 1 + }, + "l": 1 + }, "wmflabs": 1, "za": 1, "zapto": 1 @@ -11868,6 +12183,7 @@ }, "l": 1 }, + "lovable": 1, "migration": { "c": { "*": 1 @@ -12070,6 +12386,7 @@ "com": 1, "gov": 1, "hashbang": 1, + "lovable": 1, "mil": 1, "net": 1, "now": 1, From 73b3ada0cd0c6e14446ba6b776a76c374c92bade Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:58:46 +0800 Subject: [PATCH 017/298] i18n(download): download translations by bot (#520) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/option-resource.json | 65 ++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index da80cedc5..d5c47e6c2 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -1253,6 +1253,8 @@ "appearance": { "title": "المظهر", "displayWhitelist": "{input} هل يتم عرض {whitelist} في {contextMenu}", + "whitelistItem": "اختصارات القائمة البيضاء", + "contextMenu": "قائمة السياق", "displayBadgeText": "{input} هل يتم عرض {timeInfo} على {icon}", "badgeBgColor": "لون خلفية المؤقت المتواجد على الأيقونة {input}", "icon": "أيقونة الإضافة", @@ -1281,19 +1283,78 @@ }, "statistics": { "title": "الإحصائيات", + "autoPauseTrack": "{input} إيقاف تتبع الوقت إذا لم يكن هناك تفاعل {info} لمدة {maxTime}", + "noActivityInfo": "الفأرة ولوحة المفاتيح غير نشطة والشاشة ليست في وضع ملء الشاشة", "countLocalFiles": "{input} هل يتم حساب الوقت عند فتح {localFileTime} في المتصفح {info}", "localFileTime": "الملفات المحلية", "localFilesInfo": "يدعم ملفات مثل PDF، الصور، txt و json.", + "countTabGroup": "{input} هل يتم تتبع وقت مجموعات علامات التبويب {info}", + "tabGroupInfo": "عند حذف مجموعة علامات تبويب، سيتم حذف البيانات أيضًا.", + "tabGroupsPermGrant": "هذه الميزة تتطلب أذونات ذات صلة", "collectSiteName": "{input} هل يتم أخذ {siteName} {siteNameUsage} تلقائيًا عند زيارة الصفحة الرئيسية", "siteName": "اسم الموقع الإلكتروني", "siteNameUsage": "يتم تخزين البيانات محليًا فقط وسيتم عرضها بدلاً من الرابط لزيادة الوضوح. يمكنك أيضًا تخصيص اسم كل موقع.", - "fileAccessDisabled": "الوصول إلى عناوين الملفات غير مسموح به حاليًا. يرجى تفعيل الخاصية من صفحة الإدارة أولاً" + "fileAccessDisabled": "الوصول إلى عناوين الملفات غير مسموح به حاليًا. يرجى تفعيل الخاصية من صفحة الإدارة أولاً", + "fileAccessFirefox": "عذراً، هذه الميزة غير مدعومة في فايرفوكس", + "weekStart": "اليوم الأول لكل أسبوع {input}", + "weekStartAsNormal": "بشكل عادي" }, "backup": { - "title": "النسخ الاحتياطي للبيانات" + "title": "النسخ الاحتياطي للبيانات", + "type": "نوع الربط الخارجي {input}", + "client": "اسم العميل {input}", + "meta": { + "none": { + "label": "معطّل دائمًا" + }, + "gist": { + "authInfo": "مطلوب رمز وصول واحد على الأقل مع صلاحية Gist" + }, + "obsidian_local_rest_api": { + "endpointInfo": "متوفر HTTP فقط، حيث لا يمكن تكوين مشاركة الموارد عبر المنشأ (CORS) لصفحات الاضافة" + } + }, + "label": { + "path": "مسار المجلد {input}", + "account": "اسم المستخدم {input}", + "password": "كلمة المرور {input}" + }, + "operation": "إنشاء نسخة إحتياطية", + "download": { + "btn": "تحميل", + "step2": "تأكيد البيانات", + "willDownload": "ليتم تحميله", + "confirmTip": "سيتم تنزيل {size} من البيانات من [{clientName}]" + }, + "clear": { + "btn": "مسح", + "confirmTip": "سيتم حذف {rowCount} من البيانات لـ {hostCount} موقعًا التي يتتبعها [{clientName}]!" + }, + "clientTable": { + "selectTip": "اختر عميلاً", + "dataRange": "مجال البيانات", + "notSelected": "لم يتم اختيار العميل", + "current": "الحالي" + }, + "lastTimeTip": "وقت النسخ الاحتياطي الأخير: {lastTime}", + "auto": { + "label": "تفعيل النسخ الاحتياطي التلقائي", + "interval": "والتشغيل كل {input} دقيقة" + } + }, + "accessibility": { + "title": "إمكانية الوصول", + "chartDecal": "{input} هل يتم عرض الزخرفة على الرسم البياني" }, "resetButton": "إعادة الضبط", "resetSuccess": "تم إعادة الضبط إلى الإعدادات الإفتراضية بنجاح!", + "exportButton": "تصدير الإعدادات", + "importButton": "استيراد الإعدادات", + "exportSuccess": "تم تصدير الإعدادات بنجاح", + "importSuccess": "تم استيراد الإعدادات بنجاح", + "importError": "فشل الاستيراد: ملف الإعدادات غير صالح", + "importConfirm": "تم استيراد الإعدادات بنجاح، يرجى إعادة تحميل الصفحة لتطبيق التغييرات!", + "reloadButton": "إعادة التحميل", "defaultValue": "الوضع الإفتراضي: {default}" } } \ No newline at end of file From 33296f248328649d392432d72d7f09e6a11961a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:01:36 +0800 Subject: [PATCH 018/298] build(deps-dev): bump typescript from 5.8.3 to 5.9.2 (#525) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.8.3 to 5.9.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2) --- updated-dependencies: - dependency-name: typescript dependency-version: 5.9.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1753728c0..79d0d8864 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "5.8.3", + "typescript": "5.9.2", "url-loader": "^4.1.1" }, "optionalDependencies": { From ceabbd229ea24e1b0e5994c2b20ee8293117ca11 Mon Sep 17 00:00:00 2001 From: sheepie Date: Fri, 1 Aug 2025 22:04:34 +0800 Subject: [PATCH 019/298] fix: scrollbar style conflict (#524) --- src/content-script/limit/modal/index.ts | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index 5ec1085e9..b0c2ed8e5 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -1,4 +1,4 @@ -import { getUrl, sendMsg2Runtime } from '@api/chrome/runtime' +import { getRuntimeId, getUrl, sendMsg2Runtime } from '@api/chrome/runtime' import optionService from '@service/option-service' import { init as initTheme, toggle } from '@util/dark-mode' import { createApp, Ref, type App } from 'vue' @@ -46,23 +46,30 @@ const createHeader = () => { } class ScreenLocker { - private doLock() { - const ele = document?.documentElement - if (ele) { - document.documentElement.style.setProperty('overflow', 'hidden', 'important') - } - } + private styleId = `time-tracker-style-${getRuntimeId()}` + private lockedCls = `time-tracker-locked-${getRuntimeId()}` lock() { - this.doLock() - // Re-lock after 200ms to avoid being unlocked by the original website - setTimeout(() => this.doLock(), 200) + this.insertStyle() + document?.documentElement?.classList?.add?.(this.lockedCls) } unlock() { - if (document?.documentElement) { - document.documentElement.style.overflow = 'auto' - } + document?.documentElement?.classList?.remove(this.lockedCls) + } + + private insertStyle() { + if (!document) return + if (document.getElementById(this.styleId)) return + const style = document.createElement('style') + style.id = this.styleId + const css = ` + .${this.lockedCls} { + overflow: hidden !important; + } + ` + style.appendChild(document.createTextNode(css)) + document.head?.appendChild(style) } } From 797607d0145e330157dd913918b4969a73615119 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 1 Aug 2025 22:24:44 +0800 Subject: [PATCH 020/298] chore: add actiom yml to publish for all the stores --- .github/workflows/publish-all.yml | 91 +++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .github/workflows/publish-all.yml diff --git a/.github/workflows/publish-all.yml b/.github/workflows/publish-all.yml new file mode 100644 index 000000000..8cd03c827 --- /dev/null +++ b/.github/workflows/publish-all.yml @@ -0,0 +1,91 @@ +name: Publish to all stores +on: [workflow_dispatch] + +jobs: + setup: + name: Setup and build + runs-on: ubuntu-latest + outputs: + target-zip: ${{ steps.build.outputs.target-zip }} + firefox-zip: ${{ steps.build.outputs.firefox-zip }} + source-zip: ${{ steps.build.outputs.source-zip }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup NodeJS + uses: actions/setup-node@v4 + with: + node-version: "v20.11.0" + + - name: Install dependencies + run: npm install + + - name: Build for all platforms + id: build + run: | + npm run build + npm run build:firefox + echo "target-zip=market_packages/target.zip" >> $GITHUB_OUTPUT + echo "firefox-zip=market_packages/target.firefox.zip" >> $GITHUB_OUTPUT + echo "source-zip=market_packages/target.src.zip" >> $GITHUB_OUTPUT + + publish-chrome: + name: Publish to Chrome Web Store + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: market_packages + + - name: Publish to Chrome Web Store + uses: mnao305/chrome-extension-upload@v5.0.0 + with: + file-path: ${{ needs.setup.outputs.target-zip }} + extension-id: dkdhhcbjijekmneelocdllcldcpmekmm + client-id: ${{ secrets.CHROME_CLIENT_ID }} + client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} + refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} + publish: true + + publish-edge: + name: Publish to Edge Addons + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: market_packages + + - name: Publish to Edge Addons + uses: wdzeng/edge-addon@v2 + with: + zip-path: ${{ needs.setup.outputs.target-zip }} + product-id: 2a99ae83-5ec8-4ad2-aa63-9a276fc708ce + client-id: ${{ secrets.EDGE_CLIENT_ID }} + api-key: ${{ secrets.EDGE_API_KEY }} + upload-only: false + + publish-firefox: + name: Publish to Firefox Addons + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifacts + path: market_packages + + - name: Publish to Firefox Addons + uses: wdzeng/firefox-addon@v1.2.0-alpha.0 + with: + addon-guid: "{a8cf72f7-09b7-4cd4-9aaa-7a023bf09916}" + xpi-path: ${{ needs.setup.outputs.firefox-zip }} + source-file-path: ${{ needs.setup.outputs.source-zip }} + compatibility: firefox, android + jwt-issuer: ${{ secrets.FIREFOX_JWD_ISSUER }} + jwt-secret: ${{ secrets.FIREFOX_JWD_SECRET }} From 4355be3b14a65bd828c79b366709ec890fcc913b Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 1 Aug 2025 22:29:20 +0800 Subject: [PATCH 021/298] v3.5.6 --- CHANGELOG.md | 9 ++++++++- package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6717e08e..5aec6ff10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. -## [3.5.5] - 2025-0-26 + +## [3.5.6] - 2025-08-01 + +- Fixed the scrollbar style conflict with [huaban.com](https://github.com/sheepzh/time-tracker-4-browser/issues/524) +- Added some French and Arabic translations +- Upgraded Echarts to v6 with a more modern style + +## [3.5.5] - 2025-07-26 - Still that bug = = - Added some translations, thanks to [zakno52](https://github.com/zakno52) diff --git a/package.json b/package.json index 79d0d8864..43819dbd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.5", + "version": "3.5.6", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From d5a99830085309c88564e4d061995ee0a53a1d85 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 1 Aug 2025 23:24:32 +0800 Subject: [PATCH 022/298] chore: fix action --- .github/workflows/publish-all.yml | 136 ++++++++++++++++-------------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/.github/workflows/publish-all.yml b/.github/workflows/publish-all.yml index 8cd03c827..6b2bfb642 100644 --- a/.github/workflows/publish-all.yml +++ b/.github/workflows/publish-all.yml @@ -1,20 +1,17 @@ -name: Publish to all stores +name: Publish Extension to All Stores + on: [workflow_dispatch] jobs: - setup: - name: Setup and build + build: runs-on: ubuntu-latest - outputs: - target-zip: ${{ steps.build.outputs.target-zip }} - firefox-zip: ${{ steps.build.outputs.firefox-zip }} - source-zip: ${{ steps.build.outputs.source-zip }} steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 0 - - name: Setup NodeJS + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "v20.11.0" @@ -22,70 +19,87 @@ jobs: - name: Install dependencies run: npm install - - name: Build for all platforms - id: build - run: | - npm run build - npm run build:firefox - echo "target-zip=market_packages/target.zip" >> $GITHUB_OUTPUT - echo "firefox-zip=market_packages/target.firefox.zip" >> $GITHUB_OUTPUT - echo "source-zip=market_packages/target.src.zip" >> $GITHUB_OUTPUT + - name: Build for Chrome and Edge + run: npm run build + + - name: Build for Firefox + run: npm run build:firefox + + - name: Upload Chrome/Edge Artifact + uses: actions/upload-artifact@v4 + with: + name: chrome-edge-package + path: market_packages/target.zip + + - name: Upload Firefox XPI Artifact + uses: actions/upload-artifact@v4 + with: + name: firefox-xpi-package + path: market_packages/target.firefox.zip + + - name: Upload Firefox Source Artifact + uses: actions/upload-artifact@v4 + with: + name: firefox-source-package + path: market_packages/target.src.zip publish-chrome: - name: Publish to Chrome Web Store - needs: setup + needs: build runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v4 + - name: Download Chrome/Edge Artifact + uses: actions/download-artifact@v4 with: - name: build-artifacts - path: market_packages + name: chrome-edge-package - - name: Publish to Chrome Web Store + - name: Upload to Chrome Web Store uses: mnao305/chrome-extension-upload@v5.0.0 with: - file-path: ${{ needs.setup.outputs.target-zip }} - extension-id: dkdhhcbjijekmneelocdllcldcpmekmm - client-id: ${{ secrets.CHROME_CLIENT_ID }} - client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} - refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} - publish: true + file-path: target.zip + extension-id: dkdhhcbjijekmneelocdllcldcpmekmm + client-id: ${{ secrets.CHROME_CLIENT_ID }} + client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} + refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} + publish: true publish-edge: - name: Publish to Edge Addons - needs: setup + needs: build runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v4 - with: - name: build-artifacts - path: market_packages - - - name: Publish to Edge Addons - uses: wdzeng/edge-addon@v2 - with: - zip-path: ${{ needs.setup.outputs.target-zip }} - product-id: 2a99ae83-5ec8-4ad2-aa63-9a276fc708ce - client-id: ${{ secrets.EDGE_CLIENT_ID }} - api-key: ${{ secrets.EDGE_API_KEY }} - upload-only: false + - name: Download Chrome/Edge Artifact + uses: actions/download-artifact@v4 + with: + name: chrome-edge-package + + - name: Upload to Edge Add-on Store + uses: wdzeng/edge-addon@v2 + with: + zip-path: target.zip + product-id: 2a99ae83-5ec8-4ad2-aa63-9a276fc708ce + client-id: ${{ secrets.EDGE_CLIENT_ID }} + api-key: ${{ secrets.EDGE_API_KEY }} + upload-only: false publish-firefox: - name: Publish to Firefox Addons - needs: setup + needs: build runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v4 - with: - name: build-artifacts - path: market_packages - - - name: Publish to Firefox Addons - uses: wdzeng/firefox-addon@v1.2.0-alpha.0 - with: - addon-guid: "{a8cf72f7-09b7-4cd4-9aaa-7a023bf09916}" - xpi-path: ${{ needs.setup.outputs.firefox-zip }} - source-file-path: ${{ needs.setup.outputs.source-zip }} - compatibility: firefox, android - jwt-issuer: ${{ secrets.FIREFOX_JWD_ISSUER }} - jwt-secret: ${{ secrets.FIREFOX_JWD_SECRET }} + - name: Download Firefox XPI Artifact + uses: actions/download-artifact@v4 + with: + name: firefox-xpi-package + + - name: Download Firefox Source Artifact + uses: actions/download-artifact@v4 + with: + name: firefox-source-package + + - name: Upload to Firefox Add-on Store + uses: wdzeng/firefox-addon@v1.2.0-alpha.0 + with: + addon-guid: "{a8cf72f7-09b7-4cd4-9aaa-7a023bf09916}" + xpi-path: target.firefox.zip + source-file-path: target.src.zip + compatibility: firefox, android + jwt-issuer: ${{ secrets.FIREFOX_JWD_ISSUER }} + jwt-secret: ${{ secrets.FIREFOX_JWD_SECRET }} From 32a85f9915516565fd53f84056e6aeb3cb3dd6ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:18:14 +0800 Subject: [PATCH 023/298] i18n(download): download translations by bot (#527) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/i18n/message/app/analysis-resource.json | 4 +++ src/i18n/message/app/limit-resource.json | 15 ++++++-- src/i18n/message/app/option-resource.json | 36 ++++++++++++++++++- src/i18n/message/app/report-resource.json | 1 + .../message/app/site-manage-resource.json | 1 + src/i18n/message/common/button-resource.json | 2 ++ src/i18n/message/common/item-resource.json | 2 ++ src/i18n/message/common/shared-resource.json | 1 + 8 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/i18n/message/app/analysis-resource.json b/src/i18n/message/app/analysis-resource.json index 290c1ac24..71a2606da 100644 --- a/src/i18n/message/app/analysis-resource.json +++ b/src/i18n/message/app/analysis-resource.json @@ -186,6 +186,10 @@ } }, "es": { + "target": { + "site": "Sitio web", + "cate": "Categoría" + }, "common": { "focusTotal": "Tiempo total de navegación", "visitTotal": "Total de visitas", diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index 9c9057966..dfb779507 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -311,9 +311,11 @@ }, "es": { "filterDisabled": "Solo habilitados", + "wildcardTip": "¡Puedes usar comodines para coincidir con subdominios o subpáginas!", "item": { "name": "Nombre de la regla", "condition": "URL restringida", + "daily": "Límite diario", "weekly": "Límite semanal", "weekStartInfo": "El primer día de cada semana es {weekStart}, puedes cambiar este valor en las opciones de estadísticas", "delayCount": "Contagem de atraso", @@ -321,9 +323,13 @@ "visitTime": "Límite por visita", "period": "Periodos no permitidos", "enabled": "Habilitado", + "locked": "Bloqueado", "effectiveDay": "En vigor él", "delayAllowed": "Más de 5 minutos", - "delayAllowedInfo": "Si se pausa, permite un retraso temporal de 5 minutos" + "delayAllowedInfo": "Si se pausa, permite un retraso temporal de 5 minutos", + "visits": "visitas", + "or": "o", + "notEffective": "No efectivo" }, "step": { "base": "Información básica", @@ -337,10 +343,12 @@ "noUrl": "URL restringida sin completar", "noRule": "No hay reglas llenadas", "deleteConfirm": "¿Deseas eliminar la regla de {cond}?", + "lockConfirm": "Si está bloqueado, todas las operaciones requerirán verificación incluso si no se activa la regla.", "inputTestUrl": "Por favor, introduce primero el enlace URL a ser probado", "clickTestButton": "Después de ingresarla, haz clic en el botón ({buttonText})", "noRuleMatched": "La URL no sigue ninguna regla", - "rulesMatched": "La URL sigue las siguientes reglas:" + "rulesMatched": "La URL sigue las siguientes reglas:", + "timeout": "¡Tiempo se acabó! XD" }, "verification": { "inputTip": "La regla se ha activado o bloqueado. Para continuar, responda la siguiente pregunta en menos de {second} segundos: {prompt}", @@ -351,7 +359,8 @@ "incorrectAnswer": "Respuesta incorrecta", "pi": "{digitCount} dígitos de {startIndex} a {endIndex} de la parte decimal de π", "confession": "El tiempo se está fugando" - } + }, + "reminder": "¡Menos de {min} minutos hasta el límite de tiempo!" }, "de": { "filterDisabled": "Nur aktiviert", diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index d5c47e6c2..3d1b56f38 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -842,7 +842,8 @@ "off": "Siempre apagado", "timed": "Cronometrado" } - } + }, + "animationDuration": "Duración de la animación inicial del gráfico {input}" }, "statistics": { "title": "Estadísticas", @@ -851,6 +852,9 @@ "countLocalFiles": "{input} Contar el tiempo para {localFileTime} {info} en el navegador", "localFileTime": "leer un archivo local", "localFilesInfo": "Soporta archivos de tipos como PDF, imagen, TXT y JSON", + "countTabGroup": "{input} Rastrear el tiempo de los grupos de pestañas {info}", + "tabGroupInfo": "Al eliminar un grupo de pestañas, sus datos también se borrarán.", + "tabGroupsPermGrant": "Esta función requiere permisos pertinentes", "collectSiteName": "{input} Recopilar automáticamente {siteName} {siteNameUsage} al visitar la página principal del sitio", "siteName": "el nombre del sitio", "siteNameUsage": "Los datos solo se almacenan localmente y se mostrarán en lugar de la URL para aumentar el reconocimiento. Por supuesto, también puedes personalizar el nombre de cada sitio.", @@ -931,6 +935,13 @@ }, "resetButton": "Restablecer", "resetSuccess": "¡Restablecer predeterminado con éxito!", + "exportButton": "Exportar configuración", + "importButton": "Importar configuración", + "exportSuccess": "Configuración exportada correctamente", + "importSuccess": "Configuración importada correctamente", + "importError": "Error al importar: Archivo de configuración no válido", + "importConfirm": "¡Configuración importada! Recarga la página para aplicar los cambios.", + "reloadButton": "Recargar", "defaultValue": "Predeterminado: {default}" }, "de": { @@ -1299,6 +1310,28 @@ "weekStart": "اليوم الأول لكل أسبوع {input}", "weekStartAsNormal": "بشكل عادي" }, + "dailyLimit": { + "prompt": "رسالة التنبيه عند التقييد {input}", + "reminder": "{input} تذكير {minInput} دقائق قبل انتهاء الوقت", + "level": { + "label": "طريقة فك القيد أثناء التقييد {input}", + "nothing": "السماح بالفك المباشر من صفحة الإدارة", + "password": "يجب إدخال كلمة المرور لفك القيد", + "verification": "يجب إدخال رمز التحقق لفك القيد", + "passwordLabel": "كلمة المرور لفك القيد {input}", + "verificationLabel": "صعوبة رمز التحقق {input}", + "verificationDifficulty": { + "easy": "سهل", + "hard": "صعب", + "disgusting": "معقد جدًا" + }, + "strict": "عدم السماح بالفك بأي طريقة", + "strictTitle": "تأكيد العملية", + "strictContent": "عند اختيار هذا الخيار، إذا تم تقييد موقع ما، لن يُسمح لك بفك القيد يدويًا إلا بانتظار اليوم التالي. إذا لم يتم إعداد القواعد بشكل صحيح، قد تعيق روتينك اليومي!", + "pswFormLabel": "كلمة المرور", + "pswFormAgain": "إعادة الإدخال" + } + }, "backup": { "title": "النسخ الاحتياطي للبيانات", "type": "نوع الربط الخارجي {input}", @@ -1315,6 +1348,7 @@ } }, "label": { + "endpoint": "عنوان النقطة الطرفية {info} {input}", "path": "مسار المجلد {input}", "account": "اسم المستخدم {input}", "password": "كلمة المرور {input}" diff --git a/src/i18n/message/app/report-resource.json b/src/i18n/message/app/report-resource.json index c79948645..6defaef3d 100644 --- a/src/i18n/message/app/report-resource.json +++ b/src/i18n/message/app/report-resource.json @@ -124,6 +124,7 @@ }, "es": { "exportFileName": "Mi_Tiempo_De_Navegación", + "total": "Visitas totales: {visit} veces, duración total: {focus}", "batchDelete": { "noSelectedMsg": "Por favor, selecciona en la tabla primero la fila que deseas eliminar", "confirmMsg": "¡Se eliminarán {count} registros de sitios como {example} del {date}!", diff --git a/src/i18n/message/app/site-manage-resource.json b/src/i18n/message/app/site-manage-resource.json index c267bd492..122e99397 100644 --- a/src/i18n/message/app/site-manage-resource.json +++ b/src/i18n/message/app/site-manage-resource.json @@ -258,6 +258,7 @@ }, "es": { "deleteConfirmMsg": "Se eliminará {host}", + "genAliasConfirmMsg": "¿Completar automáticamente los nombres de sitios en lotes?", "column": { "type": "Tipo de sitio", "alias": "Nombre del sitio", diff --git a/src/i18n/message/common/button-resource.json b/src/i18n/message/common/button-resource.json index 37b40a69f..5f1e94eb0 100644 --- a/src/i18n/message/common/button-resource.json +++ b/src/i18n/message/common/button-resource.json @@ -129,6 +129,7 @@ }, "es": { "create": "Nuevo", + "add": "Añadir", "delete": "Eliminar", "batchDelete": "Eliminar en Lote", "modify": "Editar", @@ -144,6 +145,7 @@ "operation": "Acciones", "configuration": "Configuración", "clear": "Limpiar", + "enable": "Activar", "batchEnable": "Habilitar en Lote", "batchDisable": "Deshabilitar en Lote" }, diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index aeed037a0..6fb79f35b 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -107,7 +107,9 @@ "es": { "date": "Fecha", "host": "Dominio", + "group": "Grupo de pestañas", "focus": "Duración", + "run": "Tiempo de ejecución", "time": "Visitas", "operation": { "add2Whitelist": "Lista blanca", diff --git a/src/i18n/message/common/shared-resource.json b/src/i18n/message/common/shared-resource.json index f14e4a6a7..76cfbb05c 100644 --- a/src/i18n/message/common/shared-resource.json +++ b/src/i18n/message/common/shared-resource.json @@ -93,6 +93,7 @@ "date": "Fecha", "domain": "URL", "cate": "Categoría", + "group": "Grupo de pestañas", "notMerge": "No fusionar" } }, From 0efabbae766b6ece1fc11087b097cc66f0062a02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 19:48:46 +0800 Subject: [PATCH 024/298] build(deps): bump element-plus from 2.10.4 to 2.10.5 (#528) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.10.4 to 2.10.5. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.10.4...2.10.5) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.10.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43819dbd9..16e7ac5b0 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.10.4", + "element-plus": "2.10.5", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From c04671172924ec66071dcd73d741611a5ba13230 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 7 Aug 2025 22:25:09 +0800 Subject: [PATCH 025/298] fix: tooltip not update when backup type changed --- .../app/components/Option/components/BackupOption/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/app/components/Option/components/BackupOption/index.tsx b/src/pages/app/components/Option/components/BackupOption/index.tsx index 67341608c..eda644108 100644 --- a/src/pages/app/components/Option/components/BackupOption/index.tsx +++ b/src/pages/app/components/Option/components/BackupOption/index.tsx @@ -69,6 +69,7 @@ const _default = defineComponent((_, ctx) => { {backupType.value === 'gist' && <> 'Personal Access Token {info} {input}'} v-slots={{ info: () => {t(msg => msg.option.backup.meta.gist.authInfo)} @@ -86,6 +87,7 @@ const _default = defineComponent((_, ctx) => { } {backupType.value === 'obsidian_local_rest_api' && <> msg.option.backup.label.endpoint} v-slots={{ info: () => {t(msg => msg.option.backup.meta.obsidian_local_rest_api.endpointInfo)} @@ -129,6 +131,7 @@ const _default = defineComponent((_, ctx) => { } {backupType.value === 'web_dav' && <> msg.option.backup.label.endpoint} v-slots={{ info: () => '' }} required From e3b85aba5937bd44500f48f64ab1f30bf543cdd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:29:20 +0800 Subject: [PATCH 026/298] build(deps): bump element-plus from 2.10.5 to 2.10.6 (#529) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.10.5 to 2.10.6. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.10.5...2.10.6) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.10.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16e7ac5b0..c0d108478 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.10.5", + "element-plus": "2.10.6", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From e6c0b278eb9db8fe92c3d6434152de3051775351 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 11 Aug 2025 11:12:01 +0800 Subject: [PATCH 027/298] fix: category filtering not working (#531) --- package.json | 18 +++++++++--------- src/service/stat-service/index.ts | 9 +++++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index c0d108478..c04631572 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,17 @@ "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.0", "@crowdin/crowdin-api-client": "^1.46.0", - "@rsdoctor/rspack-plugin": "^1.1.10", - "@rspack/cli": "^1.4.10", - "@rspack/core": "^1.4.10", + "@rsdoctor/rspack-plugin": "^1.2.1", + "@rspack/cli": "^1.4.11", + "@rspack/core": "^1.4.11", "@swc/core": "^1.13.3", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.1", + "@types/chrome": "0.1.3", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.1.0", + "@types/node": "^24.2.1", "@types/punycode": "^2.1.4", - "@vue/babel-plugin-jsx": "^1.4.0", + "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", "commitlint": "^19.8.1", "css-loader": "^7.1.2", @@ -53,8 +53,8 @@ "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.15.0", - "sass": "^1.89.2", + "puppeteer": "^24.16.0", + "sass": "^1.90.0", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "ts-loader": "^9.5.2", @@ -67,7 +67,7 @@ "web-ext": "^8.9.0" }, "dependencies": { - "@element-plus/icons-vue": "^2.3.1", + "@element-plus/icons-vue": "^2.3.2", "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index 364882c46..abccd18fa 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -12,7 +12,7 @@ import siteDatabase from "@db/site-database" import statDatabase, { type StatCondition } from "@db/stat-database" import { groupBy } from "@util/array" import { judgeVirtualFast } from "@util/pattern" -import { distinctSites, SiteMap } from "@util/site" +import { CATE_NOT_SET_ID, distinctSites, SiteMap } from "@util/site" import { isGroup, isNormalSite, isSite } from "@util/stat" import { log } from "../../common/logger" import CustomizedHostMergeRuler from "../components/host-merge-ruler" @@ -112,6 +112,11 @@ interface StatService { canReadRemote(): Promise } +function filterByCateId(itemCateId: number | undefined, cateIds: number[] | undefined): boolean { + if (!cateIds?.length) return true + return cateIds.includes(itemCateId ?? CATE_NOT_SET_ID) +} + /** * Service of timer * @since 0.0.5 @@ -193,7 +198,7 @@ class StatServiceImpl implements StatService { siteRows = siteRows .filter(({ siteKey: { host: siteHost } }) => !host || host === siteHost) .filter(({ siteKey: { host: siteHost }, alias }) => !query || siteHost.includes(query) || !!alias?.includes(query)) - .filter(({ cateId }) => !cateIds?.length || (cateId && cateIds.includes(cateId))) + .filter(({ cateId }) => filterByCateId(cateId, cateIds)) // Merge by date needMergeDate && (siteRows = mergeDate(siteRows)) // Sort From 6a61ffe527ee1e8c780419aad699ad4b8cdb45b6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 11 Aug 2025 14:09:03 +0800 Subject: [PATCH 028/298] feat: add forward buttons for date range picker (#530) --- .../Analysis/components/Trend/Filter.tsx | 24 ++-- .../common/filter/DateRangeFilterItem.tsx | 113 +++++++++++++++--- 2 files changed, 103 insertions(+), 34 deletions(-) diff --git a/src/pages/app/components/Analysis/components/Trend/Filter.tsx b/src/pages/app/components/Analysis/components/Trend/Filter.tsx index ed2dadce3..2750f188a 100644 --- a/src/pages/app/components/Analysis/components/Trend/Filter.tsx +++ b/src/pages/app/components/Analysis/components/Trend/Filter.tsx @@ -5,12 +5,10 @@ * https://opensource.org/licenses/MIT */ +import DateRangeFilterItem from '@app/components/common/filter/DateRangeFilterItem' import { t } from "@app/locale" -import { dateFormat } from "@i18n/element" import { type ElementDatePickerShortcut } from "@pages/element-ui/date" -import { getDatePickerIconSlots } from "@pages/element-ui/rtl" import { daysAgo } from "@util/time" -import { ElDatePicker } from "element-plus" import { defineComponent } from "vue" import { useAnalysisTrendDateRange } from "./context" @@ -32,19 +30,13 @@ const _default = defineComponent(() => { const dateRange = useAnalysisTrendDateRange() return () => ( -
    - date.getTime() > new Date().getTime()} - format={dateFormat()} - type="daterange" - shortcuts={SHORTCUTS} - rangeSeparator="-" - clearable={false} - onUpdate:modelValue={(newVal: [Date, Date]) => dateRange.value = newVal} - v-slots={getDatePickerIconSlots()} - /> -
    + date.getTime() > new Date().getTime()} + shortcuts={SHORTCUTS} + clearable={false} + onChange={newVal => newVal && (dateRange.value = newVal)} + /> ) }) diff --git a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx index 43292e7b7..47e30b478 100644 --- a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx +++ b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx @@ -6,11 +6,15 @@ */ import { t } from "@app/locale" +import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue' import { dateFormat } from "@i18n/element" +import Flex from '@pages/components/Flex' import { type ElementDatePickerShortcut } from "@pages/element-ui/date" import { getDatePickerIconSlots } from "@pages/element-ui/rtl" -import { ElDatePicker } from "element-plus" -import { defineComponent, type StyleValue, toRaw, toRef } from "vue" +import { isRtl } from '@util/document' +import { MILL_PER_DAY } from '@util/time' +import { ElButton, ElDatePicker } from "element-plus" +import { computed, defineComponent, type StyleValue, toRaw, toRef } from "vue" const clearShortcut = (): ElementDatePickerShortcut => ({ text: t(msg => msg.button.clear), @@ -27,7 +31,38 @@ type Props = { onChange: (val: [Date, Date] | undefined) => void } +const ARROW_BTN_STYLE: StyleValue = { + padding: '8px 1px', +} + const DateRangeFilterItem = defineComponent(props => { + const rtl = isRtl() + + const backwardDate = computed(() => { + const start = props.modelValue?.[0] + if (!start) return undefined + const time = start.getTime() + return new Date(time - MILL_PER_DAY) + }) + const backwardDisabled = computed(() => { + if (!backwardDate.value) return true + const { disabledDate } = props + if (!disabledDate) return false + return disabledDate(backwardDate.value) + }) + const forwardDate = computed(() => { + const end = props.modelValue?.[1] + if (!end) return undefined + const time = end.getTime() + return new Date(time + MILL_PER_DAY) + }) + const forwardDisabled = computed(() => { + if (!forwardDate.value) return true + const { disabledDate } = props + if (!disabledDate) return false + return disabledDate(forwardDate.value) + }) + const handleChange = (newVal: [Date, Date] | undefined) => { const [start, end] = newVal || [] const isClearChosen = !start?.getTime?.() && !end?.getTime?.() @@ -43,24 +78,66 @@ const DateRangeFilterItem = defineComponent(props => { return [...value, clearShortcut()] } + const backward = () => { + const { modelValue, onChange } = props + backwardDate.value && modelValue && onChange([backwardDate.value, modelValue[1]]) + } + const forward = () => { + const { modelValue, onChange } = props + forwardDate.value && modelValue && onChange([modelValue[0], forwardDate.value]) + } + return () => ( - handleChange(toRaw(newVal))} - startPlaceholder={props.startPlaceholder} - endPlaceholder={props.endPlaceholder} - clearable={clearable.value} - style={{ - "--el-date-editor-width": "240px", - } satisfies StyleValue} - v-slots={getDatePickerIconSlots()} - /> + + + handleChange(toRaw(newVal))} + startPlaceholder={props.startPlaceholder} + endPlaceholder={props.endPlaceholder} + clearable={clearable.value} + style={{ + "--el-date-editor-width": "240px", + "--el-border-radius-base": 0, + } satisfies StyleValue} + v-slots={getDatePickerIconSlots()} + /> + + ) }, { From 741970d578de69d842ea4efd96d003eb8af068b6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 11 Aug 2025 14:37:47 +0800 Subject: [PATCH 029/298] v3.5.7 --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aec6ff10..a5567f8e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.5.7] - 2025-08-11 + +- Fixed some bugs +- Added moving buttons of date range picker ## [3.5.6] - 2025-08-01 diff --git a/package.json b/package.json index c04631572..f1bbeb9d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.6", + "version": "3.5.7", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -81,4 +81,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} From 801bcc3468b0a348a021c6a42af64c66a3c325ff Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 11 Aug 2025 21:14:18 +0800 Subject: [PATCH 030/298] fix: category merging not working for Firefox (#531) --- src/service/stat-service/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index abccd18fa..3f7e8ac17 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -234,7 +234,7 @@ class StatServiceImpl implements StatService { inclusiveRemote && (siteRows = await processRemote(siteRows, param)) // Fill site info - this.fillSite(siteRows) + await this.fillSite(siteRows) const categories = await cateDatabase.listAll() let cateRows = mergeCate(siteRows, categories) // Filter From 495b0f17c18c8660e2cbfa02ae7550060ee7a7f9 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 11 Aug 2025 21:15:39 +0800 Subject: [PATCH 031/298] v3.5.8 --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5567f8e1..e2a56bbd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.5.8] - 2025-08-11 [For Firefox] + +- Quickly fixed an error for Firefox + ## [3.5.7] - 2025-08-11 - Fixed some bugs diff --git a/package.json b/package.json index f1bbeb9d1..86955bf9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.7", + "version": "3.5.8", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -81,4 +81,4 @@ "engines": { "node": ">=20" } -} +} \ No newline at end of file From 7cbcf9b033505242f89ae112c8d0fd80b6f175bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:59:19 +0800 Subject: [PATCH 032/298] build(deps): bump element-plus from 2.10.6 to 2.10.7 (#532) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.10.6 to 2.10.7. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.10.6...2.10.7) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.10.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86955bf9f..e41895f17 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@vueuse/core": "^13.6.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.10.6", + "element-plus": "2.10.7", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From 451cc953d0577f0ca9f1a43a7240399f487c193f Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 15 Aug 2025 10:20:44 +0800 Subject: [PATCH 033/298] fix: fix issues of time limit (#533, #534) --- src/pages/app/components/Limit/LimitTable/index.tsx | 5 +++-- src/pages/app/components/Limit/context.ts | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/app/components/Limit/LimitTable/index.tsx b/src/pages/app/components/Limit/LimitTable/index.tsx index 2fb64723c..443f0d329 100644 --- a/src/pages/app/components/Limit/LimitTable/index.tsx +++ b/src/pages/app/components/Limit/LimitTable/index.tsx @@ -37,7 +37,7 @@ const _default = defineComponent(() => { }) const { - list, + list, table, changeEnabled, changeDelay, changeLocked } = useLimitTable() @@ -45,9 +45,10 @@ const _default = defineComponent(() => { return () => ( historySort.value = { prop: val?.prop, order: val?.order }} diff --git a/src/pages/app/components/Limit/context.ts b/src/pages/app/components/Limit/context.ts index 02f2bcf04..06822a393 100644 --- a/src/pages/app/components/Limit/context.ts +++ b/src/pages/app/components/Limit/context.ts @@ -18,6 +18,7 @@ export type TestInstance = { } type Context = { + table: Ref filter: Reactive list: Ref, refresh: NoArgCallback, deleteRow: ArgCallback @@ -148,6 +149,7 @@ export const useLimitProvider = () => { const test = () => testInst.value?.show?.() useProvide(NAMESPACE, { + table, filter, list, refresh, deleteRow, @@ -163,8 +165,8 @@ export const useLimitProvider = () => { export const useLimitFilter = (): Reactive => useProvider(NAMESPACE, "filter").filter -export const useLimitTable = () => useProvider( - NAMESPACE, 'list', 'refresh', 'deleteRow', 'changeEnabled', 'changeDelay', 'changeLocked' +export const useLimitTable = () => useProvider( + NAMESPACE, 'list', 'table', 'refresh', 'deleteRow', 'changeEnabled', 'changeDelay', 'changeLocked' ) export const useLimitBatch = () => useProvider( From eac3b453fc3930b2dd8031a800bcdcfdd6336a7c Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 15 Aug 2025 10:22:07 +0800 Subject: [PATCH 034/298] v3.5.9 --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a56bbd8..d8e4067c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.5.9] - 2025-08-15 + +- Fixed some bugs of time limit + ## [3.5.8] - 2025-08-11 [For Firefox] - Quickly fixed an error for Firefox diff --git a/package.json b/package.json index e41895f17..e1df322d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.8", + "version": "3.5.9", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -81,4 +81,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} From 7b43c20ac0588712ba5f9e24be89c46e67eaebf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 22:19:18 +0800 Subject: [PATCH 035/298] build(deps-dev): bump @types/chrome from 0.1.3 to 0.1.4 (#535) Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.3 to 0.1.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome) --- updated-dependencies: - dependency-name: "@types/chrome" dependency-version: 0.1.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e1df322d1..5a34774f7 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@rspack/core": "^1.4.11", "@swc/core": "^1.13.3", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.3", + "@types/chrome": "0.1.4", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.2.1", From 0f327d2964aee349811b3a61bb2646638f20eef8 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 21 Aug 2025 11:35:25 +0800 Subject: [PATCH 036/298] fix: loading miss when backup (#537) --- .vscode/extensions.json | 3 +-- package.json | 14 +++++++------- rspack/rspack.common.ts | 1 + src/pages/hooks/useRequest.ts | 4 +++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4e16bb08a..7d95f9f68 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,9 +3,8 @@ "streetsidesoftware.code-spell-checker", "editorconfig.editorconfig", "foxundermoon.shell-format", - "ms-vscode.vscode-typescript-next", ], "unwantedRecommendations": [ "esbenp.prettier-vscode", ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5a34774f7..8b61107ae 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "license": "MIT", "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/preset-env": "^7.28.0", + "@babel/preset-env": "^7.28.3", "@crowdin/crowdin-api-client": "^1.46.0", - "@rsdoctor/rspack-plugin": "^1.2.1", + "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.4.11", "@rspack/core": "^1.4.11", "@swc/core": "^1.13.3", @@ -39,7 +39,7 @@ "@types/chrome": "0.1.4", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.2.1", + "@types/node": "^24.3.0", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", @@ -53,7 +53,7 @@ "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.16.0", + "puppeteer": "^24.17.0", "sass": "^1.90.0", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", @@ -68,14 +68,14 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", - "@vueuse/core": "^13.6.0", + "@vueuse/core": "^13.7.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", "element-plus": "2.10.7", - "js-base64": "^3.7.7", + "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", - "vue": "^3.5.18", + "vue": "^3.5.19", "vue-router": "^4.5.1" }, "engines": { diff --git a/rspack/rspack.common.ts b/rspack/rspack.common.ts index cec5a7134..204b72b48 100644 --- a/rspack/rspack.common.ts +++ b/rspack/rspack.common.ts @@ -181,6 +181,7 @@ const generateOption = ({ outputPath, manifest, mode }: Option) => { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, __VUE_OPTIONS_API__: false, __VUE_PROD_DEVTOOLS__: false, + 'process.env.VUE_TRUSTED_TYPES': false, }), ] const config: Configuration = { diff --git a/src/pages/hooks/useRequest.ts b/src/pages/hooks/useRequest.ts index 6f1cb0ad0..4cc60227e 100644 --- a/src/pages/hooks/useRequest.ts +++ b/src/pages/hooks/useRequest.ts @@ -52,7 +52,9 @@ export const useRequest =

    (getter: (...p: P) => Promise | const refreshAsync = async (...p: P) => { loading.value = true - const loadingEl = await findLoadingEl(loadingTarget) + let loadingEl = await findLoadingEl(loadingTarget) + // fallback use document + !loadingEl && loadingText && (loadingEl = document.body) const loadingInstance = loadingEl ? ElLoadingService({ target: loadingEl, text: loadingText }) : null try { param.value = p From fa4c362c857dcfdeaea3f0a440b317a3b6ec7ccd Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 21 Aug 2025 11:45:31 +0800 Subject: [PATCH 037/298] v3.6.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e4067c4..b71356de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.0] - 2025-08-21 + +- Fixed a bug of backup + ## [3.5.9] - 2025-08-15 - Fixed some bugs of time limit diff --git a/package.json b/package.json index 8b61107ae..dd8811db3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.5.9", + "version": "3.6.0", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From 5bf0c63991fb8f72b7209b98399bdb12e61b3aa1 Mon Sep 17 00:00:00 2001 From: sheepie Date: Sun, 24 Aug 2025 23:41:20 +0800 Subject: [PATCH 038/298] feat: support tracking timeline (#538) --- package.json | 9 +- script/crowdin/sync-source.ts | 6 +- script/crowdin/sync-translation.ts | 6 +- src/background/content-script-handler.ts | 2 + src/background/track-server.ts | 2 +- src/content-script/index.ts | 3 + src/content-script/timeline.ts | 57 +++++ src/database/site-database.ts | 2 +- src/database/timeline-database.ts | 101 ++++++++ src/i18n/message/app/dashboard-resource.json | 14 ++ src/i18n/message/app/dashboard.ts | 7 + .../message/common/calendar-resource.json | 11 + src/i18n/message/common/calendar.ts | 1 + .../components/Summary/Calendar/Wrapper.ts | 4 +- .../Analysis/components/Trend/context.ts | 6 +- .../app/components/Dashboard/ChartTitle.tsx | 2 +- .../components/Dashboard/DashboardCard.tsx | 16 +- .../components/MonthOnMonth/Wrapper.ts | 1 + .../components/MonthOnMonth/index.tsx | 5 +- .../components/Timeline/Chart/Wrapper.ts | 237 ++++++++++++++++++ .../components/Timeline/Chart/index.tsx | 61 +++++ .../components/Timeline/Chart/useMerge.ts | 145 +++++++++++ .../Dashboard/components/Timeline/Summary.tsx | 122 +++++++++ .../Dashboard/components/Timeline/index.tsx | 21 ++ .../components/TopKVisit/Title/index.tsx | 1 - .../components/TopKVisit/Title/title.sass | 4 - .../app/components/Dashboard/dashboard.sass | 3 + src/pages/app/components/Dashboard/index.tsx | 14 +- src/pages/app/context.ts | 17 +- src/pages/app/echarts.ts | 7 +- src/pages/hooks/useEcharts.ts | 7 +- .../components/Percentage/Cate/Wrapper.ts | 4 +- .../popup/components/Percentage/chart.ts | 4 +- src/pages/popup/context.ts | 4 +- src/service/components/host-merge-ruler.ts | 2 +- src/service/site-service.ts | 6 +- src/service/stat-service/index.ts | 4 +- src/service/stat-service/merge/cate.ts | 4 +- src/service/timeline-service.ts | 36 +++ src/util/array.ts | 36 +++ src/util/time.ts | 16 +- test/util/array.test.ts | 66 ++++- types/common.d.ts | 4 +- types/timer/echarts-extend.d.ts | 7 + types/timer/merge.d.ts | 5 +- types/timer/mq.d.ts | 2 + types/timer/timeline.d.ts | 13 + 47 files changed, 1040 insertions(+), 67 deletions(-) create mode 100644 src/content-script/timeline.ts create mode 100644 src/database/timeline-database.ts create mode 100644 src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts create mode 100644 src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx create mode 100644 src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts create mode 100644 src/pages/app/components/Dashboard/components/Timeline/Summary.tsx create mode 100644 src/pages/app/components/Dashboard/components/Timeline/index.tsx create mode 100644 src/pages/app/components/Dashboard/dashboard.sass create mode 100644 src/service/timeline-service.ts create mode 100644 types/timer/echarts-extend.d.ts create mode 100644 types/timer/timeline.d.ts diff --git a/package.json b/package.json index dd8811db3..fecd77f32 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { + "pure-install": "npm install --include=optional --ignore-scripts", "dev": "rspack --config=rspack/rspack.dev.ts --watch", "dev:firefox": "rspack --config=rspack/rspack.dev.firefox.ts --watch", "dev:safari": "rspack --config=rspack/rspack.dev.safari.ts --watch", @@ -34,7 +35,7 @@ "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.4.11", "@rspack/core": "^1.4.11", - "@swc/core": "^1.13.3", + "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.4", "@types/decompress": "^4.2.7", @@ -57,7 +58,7 @@ "sass": "^1.90.0", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", - "ts-loader": "^9.5.2", + "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "5.9.2", @@ -71,7 +72,7 @@ "@vueuse/core": "^13.7.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.10.7", + "element-plus": "2.11.1", "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", @@ -81,4 +82,4 @@ "engines": { "node": ">=20" } -} +} \ No newline at end of file diff --git a/script/crowdin/sync-source.ts b/script/crowdin/sync-source.ts index d5bf0ae08..a5f3f62ce 100644 --- a/script/crowdin/sync-source.ts +++ b/script/crowdin/sync-source.ts @@ -1,5 +1,5 @@ import { type SourceFilesModel, type SourceStringsModel } from "@crowdin/crowdin-api-client" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { type CrowdinClient, getClientFromEnv, type NameKey } from "./client" import { ALL_DIRS, @@ -33,7 +33,7 @@ async function processStrings( fileContent: ItemSet, ) { const existStrings = await client.listStringsByFile(existFile.id) - const existStringsKeyMap = groupBy(existStrings, s => s.identifier, l => l[0]) + const existStringsKeyMap = toMap(existStrings, s => s.identifier) const strings2Delete: SourceStringsModel.String[] = [] const strings2Create: ItemSet = {} const strings2Update: ItemSet = {} @@ -70,7 +70,7 @@ async function processByDir(client: CrowdinClient, dir: Dir, branch: SourceFiles // 3. list all files in directory const existFiles = await client.listFilesByDirectory(directory.id) console.log("Exists file count: " + existFiles.length) - const existFileNameMap = groupBy(existFiles, f => f.name, l => l[0]) + const existFileNameMap = toMap(existFiles, f => f.name) // 4. create new files for (const [fileName, msg] of Object.entries(messages)) { if (isIgnored(dir, fileName)) { diff --git a/script/crowdin/sync-translation.ts b/script/crowdin/sync-translation.ts index 63be2b1f5..184cd3026 100644 --- a/script/crowdin/sync-translation.ts +++ b/script/crowdin/sync-translation.ts @@ -1,6 +1,6 @@ import { type SourceFilesModel } from "@crowdin/crowdin-api-client" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { exitWith } from "../util/process" import { type CrowdinClient, getClientFromEnv } from "./client" import { @@ -14,7 +14,7 @@ const CROWDIN_USER_ID_OF_OWNER = 15266594 async function processDirMessage(client: CrowdinClient, file: SourceFilesModel.File, message: ItemSet, lang: CrowdinLanguage): Promise { console.log(`Start to process dir message: fileName=${file.name}, lang=${lang}`) const strings = await client.listStringsByFile(file.id) - const stringMap = groupBy(strings, s => s.identifier, l => l[0]) + const stringMap = toMap(strings, s => s.identifier) for (const [identifier, text] of Object.entries(message)) { const string = stringMap[identifier] if (!string) { @@ -52,7 +52,7 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo } const files = await client.listFilesByDirectory(directory!.id) console.log(`find ${files.length} files of ${dir}`) - const fileMap = groupBy(files, f => f.name, l => l[0]) + const fileMap = toMap(files, f => f.name) for (const [fileName, message] of Object.entries(messages)) { console.log(`Start to sync translations of ${dir}/${fileName}`) if (isIgnored(dir, fileName)) { diff --git a/src/background/content-script-handler.ts b/src/background/content-script-handler.ts index e0f4098e4..7c39b149e 100644 --- a/src/background/content-script-handler.ts +++ b/src/background/content-script-handler.ts @@ -12,6 +12,7 @@ import optionHolder from "@service/components/option-holder" import whitelistHolder from "@service/components/whitelist-holder" import limitService from "@service/limit-service" import siteService from "@service/site-service" +import { saveTimelineEvent } from '@service/timeline-service' import { getAppPageUrl } from "@util/constant/url" import { extractFileHost, extractHostname } from "@util/pattern" import badgeManager from "./badge-manager" @@ -73,4 +74,5 @@ export default function init(dispatcher: MessageDispatcher) { const exist = await siteService.get(site) return exist?.run ? site : null }) + .register('cs.timelineEv', ev => saveTimelineEvent(ev)) } \ No newline at end of file diff --git a/src/background/track-server.ts b/src/background/track-server.ts index 3bb33787e..3fe1dac29 100644 --- a/src/background/track-server.ts +++ b/src/background/track-server.ts @@ -98,7 +98,7 @@ async function handleIncVisitEvent(param: { host: string, url: string }, sender: function splitRunTime(start: number, end: number): Record { const res: Record = {} while (start < end) { - const startOfNextDay = getStartOfDay(new Date(start)).getTime() + MILL_PER_DAY + const startOfNextDay = getStartOfDay(start).getTime() + MILL_PER_DAY const newStart = Math.min(end, startOfNextDay) const runTime = newStart - start runTime && (res[formatTimeYMD(start)] = runTime) diff --git a/src/content-script/index.ts b/src/content-script/index.ts index dc7f67b09..11e3da50d 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -9,6 +9,7 @@ import { sendMsg2Runtime } from "@api/chrome/runtime" import { initLocale } from "@i18n" import processLimit from "./limit" import printInfo from "./printer" +import processTimeline from './timeline' import NormalTracker from "./tracker/normal" import RunTimeTracker from "./tracker/run-time" @@ -73,6 +74,8 @@ async function main() { !!needPrintInfo && printInfo(host) await processLimit(url) + processTimeline() + // Increase visit count at the end await sendMsg2Runtime('cs.incVisitCount', { host, url }) } diff --git a/src/content-script/timeline.ts b/src/content-script/timeline.ts new file mode 100644 index 000000000..47eddc29d --- /dev/null +++ b/src/content-script/timeline.ts @@ -0,0 +1,57 @@ +import { sendMsg2Runtime } from '@api/chrome/runtime' + +class TimelineCollector { + private startTime: number | null = null + + /** + * Bind page visibility and focus events + */ + init(): void { + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + this.collect() + } else { + this.startTracking() + } + }) + + window.addEventListener('focus', () => this.startTracking()) + window.addEventListener('blur', () => this.collect()) + window.addEventListener('beforeunload', () => this.collect()) + + if (document.readyState === 'complete') { + this.startTracking() + } else { + window.addEventListener('load', () => this.startTracking()) + } + } + + /** + * Start tracking current page + */ + public startTracking(): void { + this.startTime = Date.now() + } + + /** + * End current session and generate event + */ + private collect(): void { + if (!this.startTime) return + const url = document?.location?.href + + url && sendMsg2Runtime('cs.timelineEv', { + start: this.startTime, + end: Date.now(), + url, + } satisfies timer.timeline.Event) + + this.startTime = null + } +} + + +export default function processTimeline() { + const collector = new TimelineCollector() + collector.init() +} \ No newline at end of file diff --git a/src/database/site-database.ts b/src/database/site-database.ts index 94726209d..19419cdf9 100644 --- a/src/database/site-database.ts +++ b/src/database/site-database.ts @@ -85,7 +85,7 @@ function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry | undefined): timer const { a, i, c, r } = entry || {} const siteInfo: timer.site.SiteInfo = { ...key } siteInfo.alias = a - siteInfo.cate = c + siteInfo.cate = c ?? CATE_NOT_SET_ID siteInfo.iconUrl = i siteInfo.run = !!r return siteInfo diff --git a/src/database/timeline-database.ts b/src/database/timeline-database.ts new file mode 100644 index 000000000..98f270f2d --- /dev/null +++ b/src/database/timeline-database.ts @@ -0,0 +1,101 @@ +import { formatTimeYMD, MILL_PER_DAY } from '@util/time' +import BaseDatabase from './common/base-database' +import { REMAIN_WORD_PREFIX } from './common/constant' + +const DB_KEY = REMAIN_WORD_PREFIX + 'TL' + +type Item = { + // start + s: number + // duration + d: number +} + +type TimelineData = { + [date: string]: { + [host: string]: Item[] + } +} + +// If two tick with the same host is near 1 sec, then merge them to one +const MERGE_THRESHOLD = 1000 + +const canMerge = (item: Item, tick: timer.timeline.Tick) => { + const { s: is, d: id } = item + const { start } = tick + return start >= is + id + && start <= id + MERGE_THRESHOLD +} + +const isConflict = (item: Item, tick: timer.timeline.Tick) => { + const { s: is, d: id } = item + const { start } = tick + return is <= start && start < is + id +} + +const merge = (data: TimelineData, tick: timer.timeline.Tick) => { + const { start, duration, host } = tick + const date = formatTimeYMD(start) + const hostData = data[date] ?? {} + const items = hostData[host] ?? [] + items.sort((a, b) => (a?.s ?? 0) - (b?.s ?? 0)) + for (const item of items) { + if (isConflict(item, tick)) { + return + } + if (canMerge(item, tick)) { + item.d = start + duration - item.s + return + } + } + // normal tick + items.push({ s: start, d: duration }) + hostData[host] = items + data[date] = hostData +} + +export const TIMELINE_LIFE_CYCLE = 3 + +const removeOutdated = (data: TimelineData, currTime: number) => { + const minDate = formatTimeYMD(currTime - MILL_PER_DAY * (TIMELINE_LIFE_CYCLE - 1)) + const keys = Object.keys(data).filter(k => k < minDate) + keys.forEach(key => delete data[key]) +} + +class TimelineDatabase extends BaseDatabase { + private async getData(): Promise { + const data = await this.storage.getOne(DB_KEY) + return data ?? {} + } + + private setData(data: TimelineData): Promise { + return this.setByKey(DB_KEY, data) + } + + async batchSave(ticks: timer.timeline.Tick[]) { + const data = await this.getData() + ticks.forEach(tick => { + merge(data, tick) + removeOutdated(data, tick.start) + }) + await this.setData(data) + } + + async getAll(): Promise { + const data = await this.getData() + const result: timer.timeline.Tick[] = [] + Object.values(data).forEach(hostData => { + Object.entries(hostData).forEach(([host, items]) => { + items.forEach(({ s: start, d: duration }) => result.push({ host, start, duration })) + }) + }) + return result + } + + async importData(_: any): Promise { + // do nothing + } +} +const timelineDatabase = new TimelineDatabase() + +export default timelineDatabase \ No newline at end of file diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json index 6add77221..1edea729a 100644 --- a/src/i18n/message/app/dashboard-resource.json +++ b/src/i18n/message/app/dashboard-resource.json @@ -15,6 +15,13 @@ }, "monthOnMonth": { "title": "浏览时间环比趋势" + }, + "timeline": { + "title": "近 {n} 天时间线", + "busyScore": "忙碌指数", + "busyScoreDesc": "与每小时浏览的总时长和网站数量有关,详细计算公式请查看源代码", + "focusScore": "专注指数", + "focusScoreDesc": "与同一网站连续浏览总时长有关,详细计算公式请查看源代码" } }, "zh_TW": { @@ -51,6 +58,13 @@ }, "monthOnMonth": { "title": "Browsing time month-on-month trend" + }, + "timeline": { + "title": "Timeline of the recent {n} days", + "busyScore": "Busy Score", + "busyScoreDesc": "Related to the total browsing time and the number of websites per hour. See the source code for calculation formula", + "focusScore": "Focus Score", + "focusScoreDesc": "Related to the total time of continuous browsing of the same website. See the source code for calculation formula" } }, "ja": { diff --git a/src/i18n/message/app/dashboard.ts b/src/i18n/message/app/dashboard.ts index 8f9a99e36..a403c180b 100644 --- a/src/i18n/message/app/dashboard.ts +++ b/src/i18n/message/app/dashboard.ts @@ -24,6 +24,13 @@ export type DashboardMessage = { monthOnMonth: { title: string } + timeline: { + title: string + busyScore: string + busyScoreDesc: string + focusScore: string + focusScoreDesc: string + } } const _default: Messages = resource diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index 8c52d00cf..d81852141 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -3,6 +3,7 @@ "weekDays": "周一|周二|周三|周四|周五|周六|周日", "months": "1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月", "dateFormat": "{y}/{m}/{d}", + "monthDateFormat": "{m}/{d}", "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", "simpleTimeFormat": "{m}/{d} {h}:{i}", "label": { @@ -25,6 +26,7 @@ "weekDays": "週一|週二|週三|週四|週五|週六|週日", "months": "1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月", "dateFormat": "{y}/{m}/{d}", + "monthDateFormat": "{m}/{d}", "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", "simpleTimeFormat": "{m}/{d} {h}:{i}", "label": { @@ -47,6 +49,7 @@ "weekDays": "Mon|Tue|Wed|Thu|Fri|Sat|Sun", "months": "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", "dateFormat": "{m}/{d}/{y}", + "monthDateFormat": "{m}/{d}", "timeFormat": "{m}/{d}/{y} {h}:{i}:{s}", "simpleTimeFormat": "{m}/{d} {h}:{i}", "label": { @@ -69,6 +72,7 @@ "weekDays": "月|火|水|木|金|土|日", "months": "1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月", "dateFormat": "{y}/{m}/{d}", + "monthDateFormat": "{m}/{d}", "timeFormat": "{y}/{m}/{d} {h}:{i}:{s}", "simpleTimeFormat": "{m}/{d} {h}:{i}", "label": { @@ -91,6 +95,7 @@ "weekDays": "Seg|Ter|Qua|Qui|Sex|Sáb|Dom", "months": "Jan|Fev|Mar|Abr|Mai|Jun|Jul|Ago|Set|Out|Nov|Dez", "dateFormat": "{d}/{m}/{y}", + "monthDateFormat": "{d}/{m}", "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}/{m} {h}:{i}", "label": { @@ -113,6 +118,7 @@ "weekDays": "Пн|Вт|Ср|Чт|Пт|Сб|Нд", "months": "січ|лют|бер|кві|тра|чер|лип|сер|вер|жов|лис|гру", "dateFormat": "{d}.{m}.{y}", + "monthDateFormat": "{d}.{m}", "timeFormat": "{d}.{m}.{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}.{m} {h}:{i}", "label": { @@ -135,6 +141,7 @@ "weekDays": "Lun|Mar|Mié|Jue|Vie|Sáb|Dom", "months": "Ene|Feb|Mar|Abr|May|Jun|Jul|Ago|Sep|Oct|Nov|Dic", "dateFormat": "{d}/{m}/{y}", + "monthDateFormat": "{d}/{m}", "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}/{m} {h}:{i}", "label": { @@ -157,6 +164,7 @@ "weekDays": "Mo|Di|Mi|Do|Fr|Sa|So", "months": "Jan|Feb|Mär|Apr|Mai|Jun|Jul|Aug|Sep|Okt|Nov|Dez", "dateFormat": "{d}.{m}.{y}", + "monthDateFormat": "{d}.{m}", "timeFormat": "{d}.{m}.{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}.{m}. {h}:{i}", "label": { @@ -179,6 +187,7 @@ "weekDays": "Lun|Mar|Mer|Jeu|Ven|Sam|Dim", "months": "Janv|Févr|Mars|Avr|Mai|Juin|Juil|Août|Sept|Oct|Nov|Déc", "dateFormat": "{d}/{m}/{y}", + "monthDateFormat": "{d}/{m}", "timeFormat": "{d}/{m}/{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}/{m} {h}:{i}", "label": { @@ -201,6 +210,7 @@ "weekDays": "Пн|Вт|Ср|Чт|Пт|Сб|Вс", "months": "Янв|Фев|Мар|Апр|Май|Июн|Июл|Авг|Сен|Окт|Ноя|Дек", "dateFormat": "{d}.{m}.{y}", + "monthDateFormat": "{d}.{m}", "timeFormat": "{d}.{m}.{y} {h}:{i}:{s}", "simpleTimeFormat": "{d}.{m} {h}:{i}", "label": { @@ -223,6 +233,7 @@ "weekDays": "الاثنين|الثلاثاء|الأربعاء|الخميس|الجمعة|السبت|الأحد", "months": "يناير|فبراير|مارس|أبريل|مايو|يونيو|يوليو|أغسطس|سبتمبر|أكتوبر|نوفمبر|ديسمبر", "dateFormat": "{d}/{m}/{y}", + "monthDateFormat": "{d}/{m}", "timeFormat": "{d} {m} {y}، {h}:{i}:{s}", "simpleTimeFormat": "{d} {m}، {h}:{i}", "label": { diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index 521e1a5f3..8e11c891d 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -12,6 +12,7 @@ export type CalendarMessage = { months: string dateFormat: string timeFormat: string + monthDateFormat: string simpleTimeFormat: string label: { startDate: string diff --git a/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts b/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts index f0dba4a9a..00d276e3a 100644 --- a/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts +++ b/src/pages/app/components/Analysis/components/Summary/Calendar/Wrapper.ts @@ -9,7 +9,7 @@ import { periodFormatter } from "@app/util/time" import { EchartsWrapper } from "@hooks/useEcharts" import { getRegularTextColor, getSecondaryTextColor } from "@pages/util/style" import weekHelper from "@service/components/week-helper" -import { groupBy, rotate } from "@util/array" +import { groupBy, rotate, toMap } from "@util/array" import { formatTime, getAllDatesBetween, MILL_PER_WEEK, parseTime } from "@util/time" import type { ComposeOption, @@ -156,7 +156,7 @@ class Wrapper extends EchartsWrapper { const endTime = new Date() const [startTime,] = await weekHelper.getWeekDate(endTime.getTime() - MILL_PER_WEEK * (colNum - 1)) const allDates = getAllDatesBetween(startTime, endTime) - const value = groupBy(rows, r => r.date, l => l?.[0]?.focus) + const value = toMap(rows, r => r.date, r => r.focus) const data: _Value[] = [] allDates.forEach((date, index) => { const dailyMills = value[date] || 0 diff --git a/src/pages/app/components/Analysis/components/Trend/context.ts b/src/pages/app/components/Analysis/components/Trend/context.ts index 2173fc7d4..627927053 100644 --- a/src/pages/app/components/Analysis/components/Trend/context.ts +++ b/src/pages/app/components/Analysis/components/Trend/context.ts @@ -7,7 +7,7 @@ import { cvt2LocaleTime } from "@app/util/time" import { useProvide, useProvider } from "@hooks" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { daysAgo, getAllDatesBetween, getDayLength, MILL_PER_DAY } from "@util/time" import { computed, onMounted, ref, watch, type Ref } from "vue" import { useAnalysisRows } from "../../context" @@ -56,12 +56,12 @@ function computeIndicatorSet( const allDates = start && end ? getAllDatesBetween(start, end) : [] if (!rows) { // No data - return [undefined, groupBy(allDates, date => date, _l => undefined)] + return [undefined, toMap(allDates, date => date, _l => undefined)] } const days = allDates.length const periodRows = rows.filter(({ date }) => allDates.includes(date ?? '')) - const periodRowMap = groupBy(periodRows, r => r.date, a => a[0]) + const periodRowMap = toMap(periodRows, r => r.date) let focusMax: DailyIndicator let visitMax: DailyIndicator let focusTotal: number, visitTotal: number, activeDay: number diff --git a/src/pages/app/components/Dashboard/ChartTitle.tsx b/src/pages/app/components/Dashboard/ChartTitle.tsx index d20180b13..9962c386e 100644 --- a/src/pages/app/components/Dashboard/ChartTitle.tsx +++ b/src/pages/app/components/Dashboard/ChartTitle.tsx @@ -3,7 +3,7 @@ import { defineComponent, h, useSlots } from "vue" const _default = defineComponent<{ text?: string }>(props => { const { default: textSlot } = useSlots() return () => ( -

    +
    {textSlot ? h(textSlot) : {props.text ?? ''}}
    ) diff --git a/src/pages/app/components/Dashboard/DashboardCard.tsx b/src/pages/app/components/Dashboard/DashboardCard.tsx index fcfa6d5b1..10206fff7 100644 --- a/src/pages/app/components/Dashboard/DashboardCard.tsx +++ b/src/pages/app/components/Dashboard/DashboardCard.tsx @@ -8,14 +8,24 @@ import { ElCard, ElCol } from "element-plus" import type { FunctionalComponent, StyleValue } from "vue" -const DashboardCard: FunctionalComponent<{ span: number }> = (props, ctx) => ( - +type Props = { + span: number + height?: number +} + +const DashboardCard: FunctionalComponent = ({ span, height }, ctx) => ( + ) +DashboardCard.displayName = 'DashboardCard' + export default DashboardCard diff --git a/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts b/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts index 583c523ff..02472e477 100644 --- a/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts +++ b/src/pages/app/components/Dashboard/components/MonthOnMonth/Wrapper.ts @@ -31,6 +31,7 @@ function optionOf(lastPeriodItems: Row[], thisPeriodItems: Row[], domWidth: numb tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, + borderWidth: 0, formatter(params: TopLevelFormatterParams) { if (!Array.isArray(params)) return '' const [thisItem, lastItem] = params.map(v => v.data as _Value).map(v => v.row) || [] diff --git a/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx b/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx index 1561b98b9..05851d77b 100644 --- a/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx +++ b/src/pages/app/components/Dashboard/components/MonthOnMonth/index.tsx @@ -45,7 +45,10 @@ const fetchData = async (): Promise<[thisMonth: Row[], lastMonth: Row[]]> => { } const _default = defineComponent(() => { - const { elRef } = useEcharts(Wrapper, fetchData) + const { elRef } = useEcharts(Wrapper, fetchData, { + // force to fix the size is different from the parent + afterInit: ew => ew.resize(), + }) return () => ( msg.dashboard.monthOnMonth.title, { k: TOP_NUM })} /> diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts new file mode 100644 index 000000000..34d1210d2 --- /dev/null +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts @@ -0,0 +1,237 @@ +import { getSeriesPalette, tooltipDot } from '@app/util/echarts' +import { EchartsWrapper } from '@hooks/useEcharts' +import { getPrimaryTextColor } from '@pages/util/style' +import { groupBy, toMap } from '@util/array' +import { formatPeriodCommon, MILL_PER_DAY, MILL_PER_HOUR, MILL_PER_MINUTE, MILL_PER_SECOND } from '@util/time' +import { + type ComposeOption, type CustomSeriesOption, type CustomSeriesRenderItem, + type DataZoomComponentOption, type GridComponentOption, type LegendComponentOption, type TooltipComponentOption +} from 'echarts' +import { graphic } from "echarts/core" +import type { Activity, MergeMethod } from './useMerge' + +export type BizData = { + activities: Activity[] + merge: MergeMethod + dates: string[] +} + +type EcOption = ComposeOption< + | CustomSeriesOption + | GridComponentOption + | TooltipComponentOption + | LegendComponentOption + | DataZoomComponentOption +> + +type LegendInfo = { + name: string + displayName?: string + color: string +} + +type MyItem = { + // host + name: string + value: [ + yIndex: number, + // seconds + start: number, + // seconds + end: number, + duration: number, + ] +} + +const formatTimeLabel = (val: number) => { + let minute = Math.floor(val / MILL_PER_MINUTE) + const hour = Math.floor(minute / 60) + minute -= hour * 60 + return `${hour.toFixed(0).padStart(2, '0')}:${minute.toFixed(0).padStart(2, '0')}` +} + +const formatStart = (startMs: number): string => { + let second = Math.floor(startMs / MILL_PER_SECOND) + let minute = Math.floor(second / 60) + second -= minute * 60 + const hour = Math.floor(minute / 60) + minute -= hour * 60 + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}` +} + +const formatDuration = (duration: number): string => { + if (duration < MILL_PER_SECOND) { + return `${duration}ms` + } else if (duration < MILL_PER_SECOND * 10) { + return `${(duration / MILL_PER_SECOND).toFixed(1)}s` + } else { + return formatPeriodCommon(duration) + } +} + +const LEGEND_WIDTH = 180 + +const collectLegends = (activities: Activity[]): LegendInfo[] => { + const colors = getSeriesPalette() + const colorLen = colors.length || 1 + + const keyNameMap = toMap(activities, a => a.seriesKey, a => a.seriesName) + let totalDuration = groupBy(activities, a => a.seriesKey, l => l.map(t => t.duration).reduce((a, b) => a + b, 0)) + return Object.entries(totalDuration) + // sort by duration desc + .sort((a, b) => b[1] - a[1]) + .map(([key], idx) => ({ + name: key, + displayName: keyNameMap[key], + color: colors[idx % colorLen], + })) +} + +const renderItem: CustomSeriesRenderItem = (params, api) => { + const categoryIndex = api.value(0) + const start = api.coord([api.value(1), categoryIndex]) + const end = api.coord([api.value(2), categoryIndex]) + + const size = api.size?.([0, 1]) + const height = ((Array.isArray(size) ? size[1] : size) ?? 0) * 0.6 + + const coordSys = params.coordSys as unknown as Cartesian2DCoordSys + + var rectShape = graphic.clipRectByRect({ + x: start[0], + y: start[1] - height / 2, + width: end[0] - start[0], + height: height + }, { + x: coordSys.x, + y: coordSys.y, + width: coordSys.width, + height: coordSys.height, + }) + + return rectShape && { + type: 'rect', + transition: ['shape', 'style'], + shape: rectShape, + style: { + fill: api.visual('color'), + }, + focus: 'series', + } +} + +const generateSeries = (biz: BizData, legendColors: Record): EcOption['series'] => { + const { activities, dates, merge } = biz + const groupBySeries = groupBy(activities, a => a.seriesKey, l => l) + + return Object.entries(groupBySeries).map(([series, list]) => { + const color = legendColors[series] + return { + name: series, + type: 'custom', + id: `${merge}-${series}`, + itemStyle: { color }, + encode: { + x: [1, 2], + y: 0, + }, + renderItem, + selectedMode: true, + data: list.map(({ date, start, duration }) => ({ + value: [dates.indexOf(date), start, start + duration, duration], + })) + } + }) +} + +class Wrapper extends EchartsWrapper { + protected replaceSeries: boolean = true + + protected async generateOption(bizData: BizData): Promise { + const { dates, activities, merge } = bizData + const domWidth = this.getDomWidth() + const gridLeft = Math.min(Math.max(30, domWidth * .05), 60) + const primaryTextColor = getPrimaryTextColor() + + const legendData = collectLegends(activities) + const legendNames = toMap(legendData, e => e.name, e => e.displayName) + const legendColor = toMap(legendData, e => e.name, e => e.color) + + const tooltipSeriesName = (key: string) => { + const name = legendNames[key] + if (merge === 'cate') return name ?? key + return name ? `${name} (${key})` : key + } + + return { + grid: { + left: gridLeft, width: domWidth - gridLeft - LEGEND_WIDTH, + bottom: '35%', height: '60%', + }, + dataZoom: { + type: 'slider', + borderColor: 'transparent', + bottom: 5, + height: 20, + labelFormatter: '', + handleStyle: { opacity: 0 }, + }, + yAxis: { + type: 'category', + data: dates, + axisTick: { + show: false, + }, + axisLine: { show: false, }, + axisLabel: { color: primaryTextColor } + }, + xAxis: { + type: 'value', + offset: 0, + max: MILL_PER_DAY, + minInterval: 10 * MILL_PER_MINUTE, + interval: 4 * MILL_PER_HOUR, + axisLabel: { + formatter: formatTimeLabel, + color: primaryTextColor, + }, + splitLine: { show: false }, + }, + tooltip: { + position: 'top', + borderWidth: 0, + formatter: params => { + const color = (params as any)?.color ?? "#000" + const param = Array.isArray(params) ? params[0] : params + const { value, seriesName } = param + const [_1, start, _2, duration] = value as MyItem['value'] + const startStr = formatStart(start) + const durStr = formatDuration(duration) + return `${tooltipDot(color)} ${seriesName ? tooltipSeriesName(seriesName) : ''}` + + `
    ${startStr} ~ ${durStr}` + }, + }, + series: generateSeries(bizData, legendColor), + legend: { + type: 'scroll', + orient: 'vertical', + align: 'left', + top: '6%', + right: 0, + textStyle: { + width: 120, + color: primaryTextColor, + overflow: 'truncate', + }, + itemWidth: 20, + data: legendData.map(({ name, color }) => ({ + name, + itemStyle: { color }, + })), + formatter: name => legendNames[name] ?? name, + } + } + } +} + +export default Wrapper \ No newline at end of file diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx new file mode 100644 index 000000000..e38032a3f --- /dev/null +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2025 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import ChartTitle from '@app/components/Dashboard/ChartTitle' +import { t } from '@app/locale' +import { TIMELINE_LIFE_CYCLE } from '@db/timeline-database' +import { Collection, Files, Link } from '@element-plus/icons-vue' +import { useShadow } from '@hooks/index' +import { useEcharts } from "@hooks/useEcharts" +import Flex from "@pages/components/Flex" +import { ElIcon, ElRadioButton, ElRadioGroup } from 'element-plus' +import { computed, defineComponent } from "vue" +import { type JSX } from 'vue/jsx-runtime' +import Wrapper, { BizData } from './Wrapper' +import { MergeMethod, useMerge } from './useMerge' + +const CHART_CONFIG: Record = { + none: , + domain: , + cate: , +} + +const TimelineChart = defineComponent<{ data: timer.timeline.Tick[] }>(props => { + const [myData] = useShadow(() => props.data) + const { merge, setMerge, activities, dates } = useMerge(myData) + const bizData = computed(() => ({ + activities: activities.value, + merge: merge.value, + dates, + })) + + const { elRef } = useEcharts(Wrapper, bizData) + + return () => ( + + + + + {t(msg => msg.dashboard.timeline.title, { n: TIMELINE_LIFE_CYCLE })} + + + + {Object.entries(CHART_CONFIG).map(([k, v]) => ( + + {v} + + ))} + + + + +
    + + ) +}, { props: ['data'] }) + +export default TimelineChart \ No newline at end of file diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts b/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts new file mode 100644 index 000000000..c5bdfe4e3 --- /dev/null +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts @@ -0,0 +1,145 @@ +import { useCategories } from '@app/context' +import { t } from '@app/locale' +import mergeRuleDatabase from '@db/merge-rule-database' +import siteDatabase from '@db/site-database' +import { TIMELINE_LIFE_CYCLE } from '@db/timeline-database' +import { useState } from '@hooks/index' +import CustomizedHostMergeRuler from '@service/components/host-merge-ruler' +import { toMap } from '@util/array' +import { CATE_NOT_SET_ID } from '@util/site' +import { formatTime, getAllDatesBetween, getStartOfDay, MILL_PER_DAY } from '@util/time' +import { onMounted, Ref, ref, watch } from 'vue' + +export type Activity = { + date: string + // offset of date (mills) + start: number + // mills + duration: number + // series + seriesKey: string + seriesName: string | undefined +} + +type ActivityInner = Omit + +export type MergeMethod = 'cate' | 'domain' | 'none' + +const isMergeMethod = (val: unknown): val is MergeMethod => { + return val === 'none' || val === 'domain' || val === 'cate' +} + +const MONTH_DATE_FORMAT = t(msg => msg.calendar.monthDateFormat) +const formatDate = (date: Date | number) => formatTime(date, MONTH_DATE_FORMAT) + +const calcOffsetOfDay = (ts: number) => { + const startOfDate = getStartOfDay(ts) + return ts - startOfDate.getTime() +} + +const genLatestDates = () => { + const now = new Date() + const start = new Date(now.getTime() - MILL_PER_DAY * (TIMELINE_LIFE_CYCLE - 1)) + return getAllDatesBetween(start, now, formatDate) +} + +async function mergeByDomain(ticks: timer.timeline.Tick[]): Promise { + // 1. merge all + const mergeRules = await mergeRuleDatabase.selectAll() + const merger = new CustomizedHostMergeRuler(mergeRules) + const allHosts = Array.from(new Set(ticks.map(t => t.host))) + const mergedMap = toMap(allHosts, h => h, h => merger.merge(h)) + + // 2. query all the merged sites' names + const allSiteKeys = Array.from(new Set(Object.values(mergedMap))) + .map((mergedHost) => ({ type: 'merged', host: mergedHost } satisfies timer.site.SiteKey)) + const allSites = await siteDatabase.getBatch(allSiteKeys) + const nameMap = toMap(allSites, s => s.host, s => s.alias) + + // 3. convert + return ticks.map(({ start, duration, host }) => { + const seriesKey = mergedMap[host] ?? host + return { + start, duration, + seriesKey, seriesName: nameMap[seriesKey], + } + }) +} + +async function mergeByCate(ticks: timer.timeline.Tick[], cateNameMap: Record): Promise { + // 1. query all the sites' category + const allSiteKeys = Array.from(new Set(ticks.map(t => t.host))) + .map(host => ({ type: 'normal', host } satisfies timer.site.SiteKey)) + const allSites = await siteDatabase.getBatch(allSiteKeys) + const siteCateMap = toMap(allSites, s => s.host, s => s.cate) + + // 2. convert + return ticks.map(({ start, duration, host }) => { + const cateId = siteCateMap[host] ?? CATE_NOT_SET_ID + return { + start, duration, + seriesKey: `${cateId}`, + seriesName: cateNameMap[cateId], + } + }) +} + +async function fillSiteName(ticks: timer.timeline.Tick[]): Promise { + // 1. query all the sites' names + const allSiteKeys = Array.from(new Set(ticks.map(t => t.host))) + .map(host => ({ type: 'normal', host } satisfies timer.site.SiteKey)) + const allSites = await siteDatabase.getBatch(allSiteKeys) + const nameMap = toMap(allSites, s => s.host, s => s.alias) + + // 2. convert + return ticks.map(({ start, duration, host }) => ({ + start, duration, + seriesKey: host, seriesName: nameMap[host], + })) +} + +async function handleMerge( + ticks: timer.timeline.Tick[], + merge: MergeMethod, + cateNameMap: Record, + dates: Set +): Promise { + let activities: ActivityInner[] = [] + if (merge === 'domain') { + activities = await mergeByDomain(ticks) + } else if (merge === 'cate') { + activities = await mergeByCate(ticks, cateNameMap) + } else { + activities = await fillSiteName(ticks) + } + const result: Activity[] = [] + activities.forEach(act => { + let actStart = act.start + const date = formatDate(actStart) + if (!dates.has(date)) return + + const start = calcOffsetOfDay(act.start) + result.push({ ...act, date, start }) + }) + return result +} + +export const useMerge = (ticks: Ref) => { + const dates = genLatestDates() + const merge = ref('none') + const { cateNameMap } = useCategories() + const setMerge = (val: unknown) => isMergeMethod(val) && (merge.value = val) + + const [activities, setActivities] = useState([]) + + const refreshActivities = async () => { + const newVal = await handleMerge(ticks.value, merge.value, cateNameMap.value, new Set(dates)) + setActivities(newVal) + } + + watch([ticks, merge, cateNameMap], refreshActivities) + + onMounted(refreshActivities) + + return { merge, setMerge, activities, dates } +} \ No newline at end of file diff --git a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx new file mode 100644 index 000000000..af5196edb --- /dev/null +++ b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx @@ -0,0 +1,122 @@ +import { t } from '@app/locale' +import { InfoFilled } from '@element-plus/icons-vue' +import Flex from '@pages/components/Flex' +import { groupBy } from '@util/array' +import { MILL_PER_HOUR, MILL_PER_MINUTE } from '@util/time' +import { ElIcon, ElRate, ElText, ElTooltip } from 'element-plus' +import { computed, defineComponent, FunctionalComponent } from 'vue' + +type ScoreValue = 1 | 2 | 3 | 4 | 5 + +type AnalysisResult = { + busy: ScoreValue + focus: ScoreValue +} + +const defaultResult = (): AnalysisResult => ({ + busy: 1, + focus: 1, +}) + +const cvtRaw2Score = (raw: number): ScoreValue => { + if (raw < 1.5) return 1 + else if (raw < 2.5) return 2 + else if (raw < 3.5) return 3 + else if (raw < 4.5) return 4 + else return 5 +} + +const computeSessionScore = (ticks: timer.timeline.Tick[], hourCount: number) => { + let continuousSessions = 0 + let currentSession: timer.timeline.Tick[] = [] + + ticks.sort((a, b) => a.start - b.start).forEach(currentTick => { + if (!currentSession.length) { + currentSession.push(currentTick) + return + } + + const prevTick = currentSession[currentSession.length - 1] + const gap = currentTick.start - (prevTick.start + prevTick.duration) + + if (gap <= MILL_PER_MINUTE * 3) { + currentSession.push(currentTick) + } else { + if (currentSession.length > 1) continuousSessions++ + currentSession = [currentTick] + } + }) + + if (currentSession.length > 1) continuousSessions++ + + const sessionDensity = continuousSessions / Math.max(1, hourCount / 10) + return Math.min(sessionDensity, 1) +} + +const analyze = (ticks: timer.timeline.Tick[]): AnalysisResult => { + if (!ticks.length) return defaultResult() + + const minTime = ticks.map(t => t.start).sort((a, b) => a - b)[0]! + const maxTime = ticks.map(t => t.start + t.duration).sort((a, b) => b - a)[0]! + const totalRange = maxTime - minTime + const totalActiveTime = ticks.reduce((sum, tick) => sum + tick.duration, 0) + + // { hourStart: hosts } + const hourlyData = groupBy(ticks, + t => Math.floor(t.start / MILL_PER_HOUR) * MILL_PER_HOUR, + l => new Set(l.map(t => t.host)), + ) + + // busyScore = timeDensity * 0.6 + hostCountPerHour * 0.4 + const timeDensity = totalActiveTime / totalRange + const maxHostCount = Object.values(hourlyData).map(hosts => hosts.size).sort((a, b) => b - a)[0]! + const busyRawScore = (timeDensity * 0.6 + (Math.min(maxHostCount / 10, 1) * 0.4)) * 5 + + // focusScore = duration * 0.7 + session * 0.3 + const avgDuration = totalActiveTime / ticks.length + const avgDurationScore = Math.min(avgDuration / (2 * MILL_PER_MINUTE), 1) + const sessionScore = computeSessionScore(ticks, Object.keys(hourlyData).length) + const focusRawScore = (avgDurationScore * 0.7 + sessionScore * 0.3) * 5 + + return { + busy: cvtRaw2Score(busyRawScore), + focus: cvtRaw2Score(focusRawScore), + } +} + +const Score: FunctionalComponent<{ score: ScoreValue, label: string, desc: string }> = ({ score, label, desc }) => ( + + + {`${label} `} + + + + + + + + + + +) + +const Summary = defineComponent<{ data: timer.timeline.Tick[] }>(props => { + const ticks = computed(() => analyze(props.data)) + + return () => ( + + msg.dashboard.timeline.busyScore)} + desc={t(msg => msg.dashboard.timeline.busyScoreDesc)} + /> + msg.dashboard.timeline.focusScore)} + desc={t(msg => msg.dashboard.timeline.focusScoreDesc)} + /> + + ) +}, { props: ['data'] }) + +export default Summary \ No newline at end of file diff --git a/src/pages/app/components/Dashboard/components/Timeline/index.tsx b/src/pages/app/components/Dashboard/components/Timeline/index.tsx new file mode 100644 index 000000000..346f0f6be --- /dev/null +++ b/src/pages/app/components/Dashboard/components/Timeline/index.tsx @@ -0,0 +1,21 @@ +import timelineDatabase from '@db/timeline-database' +import { useRequest } from '@hooks' +import { defineComponent } from 'vue' +import DashboardCard from '../../DashboardCard' +import TimelineChart from './Chart' +import Summary from './Summary' + +const Timeline = defineComponent<{ height: number }>(({ height }) => { + const { data } = useRequest(() => timelineDatabase.getAll(), { defaultValue: [] }) + + return () => <> + + + + + + + +}, { props: ['height'] }) + +export default Timeline \ No newline at end of file diff --git a/src/pages/app/components/Dashboard/components/TopKVisit/Title/index.tsx b/src/pages/app/components/Dashboard/components/TopKVisit/Title/index.tsx index 2e36f4732..1cdf07a43 100644 --- a/src/pages/app/components/Dashboard/components/TopKVisit/Title/index.tsx +++ b/src/pages/app/components/Dashboard/components/TopKVisit/Title/index.tsx @@ -44,7 +44,6 @@ const Title = defineComponent(() => { })} filter.topKChartType = val as TopKChartType} diff --git a/src/pages/app/components/Dashboard/components/TopKVisit/Title/title.sass b/src/pages/app/components/Dashboard/components/TopKVisit/Title/title.sass index dece9bf82..303183d8a 100644 --- a/src/pages/app/components/Dashboard/components/TopKVisit/Title/title.sass +++ b/src/pages/app/components/Dashboard/components/TopKVisit/Title/title.sass @@ -1,8 +1,4 @@ -.dashboard-top-k-chart-filter - .el-radio-button__inner - padding: 3px 5px - .el-title-select margin-inline: 3px width: 28px diff --git a/src/pages/app/components/Dashboard/dashboard.sass b/src/pages/app/components/Dashboard/dashboard.sass new file mode 100644 index 000000000..a8dfe61a5 --- /dev/null +++ b/src/pages/app/components/Dashboard/dashboard.sass @@ -0,0 +1,3 @@ +.dashboard-chart-title + .el-radio-button__inner + padding: 3px 5px diff --git a/src/pages/app/components/Dashboard/index.tsx b/src/pages/app/components/Dashboard/index.tsx index a35ddfcdc..ce484f67d 100644 --- a/src/pages/app/components/Dashboard/index.tsx +++ b/src/pages/app/components/Dashboard/index.tsx @@ -13,13 +13,15 @@ import Flex from "@pages/components/Flex" import metaService from "@service/meta-service" import { REVIEW_PAGE } from "@util/constant/url" import { ElRow, ElScrollbar } from "element-plus" -import { computed, defineComponent, FunctionalComponent } from "vue" +import { defineComponent, FunctionalComponent } from "vue" import { useRouter } from "vue-router" import ContentContainer from "../common/ContentContainer" import Calendar from "./components/Calendar" import Indicator from "./components/Indicator" import MonthOnMonth from "./components/MonthOnMonth" +import Timeline from './components/Timeline' import TopKVisit from "./components/TopKVisit" +import "./dashboard.sass" import DashboardCard from './DashboardCard' const ROW_GUTTER = 15 @@ -47,7 +49,6 @@ const _default = defineComponent(() => { const mediaSize = useMediaSize() const isXs = useXsState() - const showCalendar = computed(() => mediaSize.value >= MediaSize.lg) return () => ( @@ -76,9 +77,12 @@ const _default = defineComponent(() => { } - - - + {mediaSize.value >= MediaSize.lg && <> + + + + + } {showRate.value ? ( diff --git a/src/pages/app/context.ts b/src/pages/app/context.ts index 84336ad17..d99327997 100644 --- a/src/pages/app/context.ts +++ b/src/pages/app/context.ts @@ -1,17 +1,28 @@ import { useProvide, useProvider, useRequest } from "@hooks" import cateService from "@service/cate-service" -import { type Ref } from "vue" +import { toMap } from '@util/array' +import { CATE_NOT_SET_ID } from '@util/site' +import { computed, type Ref } from "vue" +import { t } from './locale' type AppContextValue = { categories: Ref refreshCategories: () => void + cateNameMap: Ref> } const NAMESPACE = '_' export const initAppContext = () => { const { data: categories, refresh: refreshCategories } = useRequest(() => cateService.listAll(), { defaultValue: [] }) - useProvide(NAMESPACE, { categories, refreshCategories }) + const cateNameMap = computed(() => { + const map = toMap(categories.value, c => c.id, c => c.name) + map[CATE_NOT_SET_ID] = t(msg => msg.shared.cate.notSet) + return map + }) + useProvide(NAMESPACE, { categories, refreshCategories, cateNameMap }) } -export const useCategories = () => useProvider(NAMESPACE, "categories", "refreshCategories") +export const useCategories = () => useProvider( + NAMESPACE, "categories", "refreshCategories", "cateNameMap" +) diff --git a/src/pages/app/echarts.ts b/src/pages/app/echarts.ts index b57423e3b..adaea40a2 100644 --- a/src/pages/app/echarts.ts +++ b/src/pages/app/echarts.ts @@ -1,6 +1,7 @@ -import { BarChart, EffectScatterChart, HeatmapChart, LineChart, PieChart, ScatterChart } from "echarts/charts" +import { BarChart, CustomChart, EffectScatterChart, HeatmapChart, LineChart, PieChart, ScatterChart } from "echarts/charts" import { AriaComponent, + DataZoomComponent, GridComponent, LegendComponent, TitleComponent, @@ -13,7 +14,7 @@ import { CanvasRenderer } from "echarts/renderers" export const initEcharts = () => { use([ CanvasRenderer, - AriaComponent, GridComponent, TooltipComponent, TitleComponent, VisualMapComponent, LegendComponent, - BarChart, PieChart, LineChart, HeatmapChart, ScatterChart, EffectScatterChart, + AriaComponent, GridComponent, TooltipComponent, TitleComponent, VisualMapComponent, LegendComponent, DataZoomComponent, + BarChart, PieChart, LineChart, HeatmapChart, ScatterChart, EffectScatterChart, CustomChart, ]) } \ No newline at end of file diff --git a/src/pages/hooks/useEcharts.ts b/src/pages/hooks/useEcharts.ts index 75bf0b23c..c979700a3 100644 --- a/src/pages/hooks/useEcharts.ts +++ b/src/pages/hooks/useEcharts.ts @@ -21,6 +21,10 @@ export abstract class EchartsWrapper { * true if need to re-generate option while size changing, or false */ protected isSizeSensitize: boolean = false + /** + * true if need to clear all the before series when setOption + */ + protected replaceSeries: boolean = false private lastBizOption: BizOption | undefined init(container: HTMLDivElement) { @@ -40,7 +44,8 @@ export abstract class EchartsWrapper { await this.postChartOption(option) - this.instance?.setOption(option, { notMerge: false }) + const replaceMerge = this.replaceSeries ? ['series'] : undefined + this.instance?.setOption(option, { notMerge: false, replaceMerge }) } protected async postChartOption(option: EchartsOption & BaseEchartsOption) { diff --git a/src/pages/popup/components/Percentage/Cate/Wrapper.ts b/src/pages/popup/components/Percentage/Cate/Wrapper.ts index 172c35113..c683e3dae 100644 --- a/src/pages/popup/components/Percentage/Cate/Wrapper.ts +++ b/src/pages/popup/components/Percentage/Cate/Wrapper.ts @@ -3,7 +3,7 @@ import { getInfoColor, getPrimaryTextColor } from "@pages/util/style" import { t } from "@popup/locale" import cateService from "@service/cate-service" import { mergeDate } from "@service/stat-service/merge/date" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { CATE_NOT_SET_ID } from "@util/site" import { isCate } from "@util/stat" import { type PieSeriesOption } from "echarts/charts" @@ -95,7 +95,7 @@ export default class SiteWrapper extends EchartsWrapper c.id, l => l?.[0]?.name) + const cateNameMap = toMap(cates, c => c.id, c => c.name) cateNameMap[CATE_NOT_SET_ID] = t(msg => msg.shared.cate.notSet) let legend: LegendComponentOption = { diff --git a/src/pages/popup/components/Percentage/chart.ts b/src/pages/popup/components/Percentage/chart.ts index 24864472b..b27ac1d78 100644 --- a/src/pages/popup/components/Percentage/chart.ts +++ b/src/pages/popup/components/Percentage/chart.ts @@ -2,7 +2,7 @@ import { createTab } from "@api/chrome/tab" import { getCssVariable, getPrimaryTextColor, getSecondaryTextColor } from "@pages/util/style" import { calJumpUrl } from "@popup/common" import { t } from "@popup/locale" -import { groupBy, sum } from "@util/array" +import { sum, toMap } from "@util/array" import { IS_SAFARI } from "@util/constant/environment" import { isRtl } from "@util/document" import { generateSiteLabel } from "@util/site" @@ -199,7 +199,7 @@ type CustomOption = Pick< export function generateSiteSeriesOption(rows: timer.stat.Row[], result: PercentageResult, customOption: CustomOption): PieSeriesOption { const { displaySiteName, query: { dimension }, itemCount, groups } = result || {} - const groupMap = groupBy(groups, g => g.id, l => l[0]) + const groupMap = toMap(groups, g => g.id) const chartRows = cvt2ChartRows(rows, dimension, itemCount) const iconRich: PieLabelRichOption = {} diff --git a/src/pages/popup/context.ts b/src/pages/popup/context.ts index 65980e0ce..3ed3d6e07 100644 --- a/src/pages/popup/context.ts +++ b/src/pages/popup/context.ts @@ -2,7 +2,7 @@ import { useLocalStorage, useRequest } from "@hooks" import { useProvide, useProvider } from "@hooks/useProvider" import cateService from "@service/cate-service" import optionService from "@service/option-service" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { isDarkMode, toggle } from "@util/dark-mode" import { CATE_NOT_SET_ID } from "@util/site" import { reactive, type Reactive, ref, type Ref, toRaw, watch } from "vue" @@ -40,7 +40,7 @@ export const initPopupContext = (): Ref => { const { data: cateNameMap } = useRequest(async () => { const categories = await cateService.listAll() - const result = groupBy(categories || [], c => c?.id, l => l?.[0]?.name) + const result = toMap(categories ?? [], c => c.id, c => c.name) result[CATE_NOT_SET_ID] = t(msg => msg.shared.cate.notSet) return result }, { defaultValue: {} }) diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index b8afce9a2..046d06495 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -53,7 +53,7 @@ function convert(dbItem: timer.merge.Rule): RegRuleItem | [string, string | numb } } -export default class CustomizedHostMergeRuler implements timer.merge.Merger { +export default class CustomizedHostMergeRuler { private noRegMergeRules: { [origin: string]: string | number } = {} private regulars: RegRuleItem[] = [] diff --git a/src/service/site-service.ts b/src/service/site-service.ts index 55c1bf194..35d06c3eb 100644 --- a/src/service/site-service.ts +++ b/src/service/site-service.ts @@ -7,7 +7,7 @@ import { listTabs, sendMsg2Tab } from "@api/chrome/tab" import siteDatabase, { type SiteCondition } from "@db/site-database" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { identifySiteKey, SiteMap, supportCategory } from "@util/site" import { slicePageResult } from "./components/page-info" @@ -128,7 +128,7 @@ class SiteService { if (!keys?.length) return const allSites = await siteDatabase.getBatch(keys) - const siteMap = groupBy(allSites, identifySiteKey, l => l?.[0]) + const siteMap = toMap(allSites, identifySiteKey) const toSave = keys.map(k => { const s = siteMap[identifySiteKey(k)] @@ -150,7 +150,7 @@ class SiteService { */ async get(siteKey: timer.site.SiteKey): Promise { const info = await siteDatabase.get(siteKey) - return info || siteKey + return info ?? siteKey } } diff --git a/src/service/stat-service/index.ts b/src/service/stat-service/index.ts index 3f7e8ac17..32e7cc27c 100644 --- a/src/service/stat-service/index.ts +++ b/src/service/stat-service/index.ts @@ -10,7 +10,7 @@ import mergeRuleDatabase from "@db/merge-rule-database" import cateDatabase from "@db/site-cate-database" import siteDatabase from "@db/site-database" import statDatabase, { type StatCondition } from "@db/stat-database" -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { judgeVirtualFast } from "@util/pattern" import { CATE_NOT_SET_ID, distinctSites, SiteMap } from "@util/site" import { isGroup, isNormalSite, isSite } from "@util/stat" @@ -275,7 +275,7 @@ class StatServiceImpl implements StatService { } = param ?? {} const list = await statDatabase.selectGroup({ date }) const groups = await listAllGroups() - const groupMap = groupBy(groups, g => g.id, l => l[0]) + const groupMap = toMap(groups, g => g.id) let rows: timer.stat.GroupRow[] = list.map(({ date, time, focus, run, host }) => { const groupKey = parseInt(host) const { title, color } = groupMap[groupKey] ?? {} diff --git a/src/service/stat-service/merge/cate.ts b/src/service/stat-service/merge/cate.ts index 15f236724..a6dfbd727 100644 --- a/src/service/stat-service/merge/cate.ts +++ b/src/service/stat-service/merge/cate.ts @@ -1,9 +1,9 @@ -import { groupBy } from "@util/array" +import { toMap } from "@util/array" import { CATE_NOT_SET_ID } from "@util/site" import { mergeResult } from "./common" export function mergeCate(origin: timer.stat.SiteRow[], cates: timer.site.Cate[]): timer.stat.CateRow[] { - const cateNameMap = groupBy(cates, c => c.id, l => l[0]?.name) + const cateNameMap = toMap(cates, c => c.id, c => c.name) const rowMap: Record> = {} origin.forEach(ele => { if (ele.siteKey.type !== 'normal') return diff --git a/src/service/timeline-service.ts b/src/service/timeline-service.ts new file mode 100644 index 000000000..0da113777 --- /dev/null +++ b/src/service/timeline-service.ts @@ -0,0 +1,36 @@ +import timelineDatabase from '@db/timeline-database' +import { extractHostname } from '@util/pattern' + +const split2Durations = (start: number, end: number): [start: number, duration: number][] => { + const result: [start: number, duration: number][] = [] + + if (start >= end) { + return result + } + + let currentStart = start + + while (currentStart < end) { + const currentDate = new Date(currentStart) + const dayStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate()).getTime() + const nextDayStart = dayStart + 24 * 60 * 60 * 1000 + const segmentEnd = Math.min(nextDayStart, end) + + const duration = segmentEnd - currentStart + result.push([currentStart, duration]) + + currentStart = segmentEnd + } + + return result +} + +export async function saveTimelineEvent(ev: timer.timeline.Event): Promise { + const { start, end, url } = ev + const { host } = extractHostname(url) + if (!host) return + + const durations = split2Durations(start, end) + const ticks: timer.timeline.Tick[] = durations.map(([start, duration]) => ({ start, duration, host })) + await timelineDatabase.batchSave(ticks) +} diff --git a/src/util/array.ts b/src/util/array.ts index 0f859701d..21686c068 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -34,6 +34,42 @@ export function groupBy( return result } +export function toMap( + arr: E[], + keyFun: (e: E, idx: number) => K | undefined | null, +): Record + +export function toMap( + arr: E[], + keyFun: (e: E, idx: number) => K | undefined | null, + valFunc: (t: E, key: K) => V, +): Record + +/** + * To map + * + * @param arr original array + * @param keyFunc key generator + * @param valFunc value generator + * @returns k-v map + * @since 3.6.1 + */ +export function toMap( + arr: E[], + keyFunc: (e: E, idx: number) => K | undefined | null, + valFunc?: (t: E, key: K) => V, +): Record { + const result: Record = {} + arr.forEach((e, i) => { + const key = keyFunc(e, i) + if (key === undefined || key === null) { + return + } + result[key] = valFunc ? valFunc(e, key) : e + }) + return result +} + /** * Rotate the array without new one returned * diff --git a/src/util/time.ts b/src/util/time.ts index 2a84c933a..2a0f06996 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -178,11 +178,10 @@ export function getMonthTime(target: Date): [Date, Date] { * @returns the start of this day * @since 1.0.0 */ -export function getStartOfDay(target: Date) { - const currentMonth = target.getMonth() - const currentYear = target.getFullYear() - const currentDate = target.getDate() - return new Date(currentYear, currentMonth, currentDate) +export function getStartOfDay(target: Date | number) { + const date = new Date(target) + date.setHours(0, 0, 0, 0) + return date } /** @@ -227,14 +226,15 @@ export function getDayLength(dateStart: Date, dateEnd: Date): number { * [] if 2022-06-10 00:00:00 to 2022-06-09 00:00:01 * [20221110, 20221111] if 2022-11-10 08:00:00 to 2022-11-11 00:00:01 */ -export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { +export function getAllDatesBetween(dateStart: Date, dateEnd: Date, formatter?: Converter): string[] { let cursor = new Date(dateStart) let dates: string[] = [] + formatter = formatter ?? formatTimeYMD do { - dates.push(formatTimeYMD(cursor)) + dates.push(formatter(cursor)) cursor = new Date(cursor.getTime() + MILL_PER_DAY) } while (cursor.getTime() < dateEnd.getTime()) - isSameDay(cursor, dateEnd) && dates.push(formatTimeYMD(dateEnd)) + isSameDay(cursor, dateEnd) && dates.push(formatter(dateEnd)) return dates } diff --git a/test/util/array.test.ts b/test/util/array.test.ts index 52e76bcb2..1f7e04f47 100644 --- a/test/util/array.test.ts +++ b/test/util/array.test.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { allMatch, anyMatch, average, groupBy, rotate, sum } from "@util/array" +import { allMatch, anyMatch, average, groupBy, rotate, sum, toMap } from "@util/array" describe("util/array", () => { @@ -67,3 +67,67 @@ describe("util/array", () => { expect(anyMatch(arr, a => a > 100)).toBeFalsy() }) }) + +describe('toMap', () => { + + const users = [ + { id: 1, name: 'Alice', role: 'admin' }, + { id: 2, name: 'Bob', role: 'user' }, + { id: 3, name: 'Charlie', role: 'guest' }, + ] + + const products = [ + { id: 'p1', name: 'Laptop', price: 1200 }, + { id: 'p2', name: 'Mouse', price: 25 }, + { id: 'p3', name: 'Keyboard', price: 75 }, + ] + + test('should create a map with array elements as values when valFunc is not provided', () => { + const userMap = toMap(users, u => u.id) + expect(userMap).toEqual({ + 1: { id: 1, name: 'Alice', role: 'admin' }, + 2: { id: 2, name: 'Bob', role: 'user' }, + 3: { id: 3, name: 'Charlie', role: 'guest' }, + }) + }) + + test('should create a map with transformed values when valFunc is provided', () => { + const userRolesMap = toMap(users, u => u.id, u => u.role) + expect(userRolesMap).toEqual({ + 1: 'admin', + 2: 'user', + 3: 'guest', + }) + }) + + test('should handle string keys correctly', () => { + const productMap = toMap(products, p => p.id) + + expect(productMap).toEqual({ + 'p1': { id: 'p1', name: 'Laptop', price: 1200 }, + 'p2': { id: 'p2', name: 'Mouse', price: 25 }, + 'p3': { id: 'p3', name: 'Keyboard', price: 75 }, + }) + }) + + test('should return an empty map for an empty array', () => { + const emptyArray: any[] = [] + const emptyMap = toMap(emptyArray, i => i.id) + expect(emptyMap).toEqual({}) + }) + + test('should overwrite existing keys with later elements', () => { + const usersWithDuplicateKey = [ + { id: 1, name: 'Alice', role: 'admin' }, + { id: 2, name: 'Bob', role: 'user' }, + { id: 1, name: 'John', role: 'guest' }, // ID 1 重复 + ] + + const userMap = toMap(usersWithDuplicateKey, u => u.id) + + expect(userMap).toEqual({ + 1: { id: 1, name: 'John', role: 'guest' }, + 2: { id: 2, name: 'Bob', role: 'user' }, + }) + }) +}) diff --git a/types/common.d.ts b/types/common.d.ts index 97ccaaa6e..4fc1aef74 100644 --- a/types/common.d.ts +++ b/types/common.d.ts @@ -48,4 +48,6 @@ declare type Getter = () => T | Promise declare type NoArgCallback = () => void -declare type ArgCallback = (val: T) => void \ No newline at end of file +declare type ArgCallback = (val: T) => void + +declare type Converter = (val: T) => R \ No newline at end of file diff --git a/types/timer/echarts-extend.d.ts b/types/timer/echarts-extend.d.ts new file mode 100644 index 000000000..f606a2138 --- /dev/null +++ b/types/timer/echarts-extend.d.ts @@ -0,0 +1,7 @@ +interface Cartesian2DCoordSys { + type: 'cartesian2d' + x: number + y: number + width: number + height: number +} \ No newline at end of file diff --git a/types/timer/merge.d.ts b/types/timer/merge.d.ts index 753e562c0..2e800e6c2 100644 --- a/types/timer/merge.d.ts +++ b/types/timer/merge.d.ts @@ -6,13 +6,10 @@ declare namespace timer.merge { origin: string /** * The merge result - * + * * + Empty string means equals to the origin host * + Number means the count of kept dots, must be natural number (int & >=0) */ merged: string | number } - interface Merger { - merge(host: string): string - } } diff --git a/types/timer/mq.d.ts b/types/timer/mq.d.ts index dc3024d27..bee03ef1c 100644 --- a/types/timer/mq.d.ts +++ b/types/timer/mq.d.ts @@ -36,6 +36,8 @@ declare namespace timer.mq { | "cs.idleChange" // @since 3.2.0 | "cs.getRunSites" + // @since 3.6.1 + | "cs.timelineEv" type ResCode = "success" | "fail" | "ignore" diff --git a/types/timer/timeline.d.ts b/types/timer/timeline.d.ts new file mode 100644 index 000000000..e2a0a10de --- /dev/null +++ b/types/timer/timeline.d.ts @@ -0,0 +1,13 @@ +declare namespace timer.timeline { + type Event = { + start: number + end: number + url: string + } + + type Tick = { + start: number + duration: number + host: string + } +} \ No newline at end of file From 0d7b303d342640be7153f13e1a840879b7cd36c5 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 24 Aug 2025 23:43:16 +0800 Subject: [PATCH 039/298] v3.6.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b71356de9..569393c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.1] - 2025-08-24 + +- Supported tracking timeline (#538) + ## [3.6.0] - 2025-08-21 - Fixed a bug of backup diff --git a/package.json b/package.json index fecd77f32..77549eaaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.0", + "version": "3.6.1", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From 77320c2c7303bf2dfe349fcc1c00014ab21b77a9 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 25 Aug 2025 21:01:29 +0800 Subject: [PATCH 040/298] feat: click to toggle series for timeline --- .../components/Timeline/Chart/Wrapper.ts | 2 +- .../components/Timeline/Chart/index.tsx | 43 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts index 34d1210d2..858f5b3b1 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts @@ -16,7 +16,7 @@ export type BizData = { dates: string[] } -type EcOption = ComposeOption< +export type EcOption = ComposeOption< | CustomSeriesOption | GridComponentOption | TooltipComponentOption diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx index e38032a3f..85cc5e3be 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx @@ -12,11 +12,12 @@ import { Collection, Files, Link } from '@element-plus/icons-vue' import { useShadow } from '@hooks/index' import { useEcharts } from "@hooks/useEcharts" import Flex from "@pages/components/Flex" +import { type ECElementEvent, type ECharts } from "echarts/core" import { ElIcon, ElRadioButton, ElRadioGroup } from 'element-plus' import { computed, defineComponent } from "vue" import { type JSX } from 'vue/jsx-runtime' -import Wrapper, { BizData } from './Wrapper' -import { MergeMethod, useMerge } from './useMerge' +import Wrapper, { EcOption, type BizData } from './Wrapper' +import { useMerge, type MergeMethod } from './useMerge' const CHART_CONFIG: Record = { none: , @@ -24,6 +25,36 @@ const CHART_CONFIG: Record = { cate: , } +const extractLegendSelected = (legends: EcOption['legend']): Record => { + if (!legends) return {} + const result: Record = {} + const allLegends = new Set() + const legendArr = Array.isArray(legends) ? legends : [legends] + legendArr.forEach(({ data, selected }) => { + data?.map(d => { + const name = typeof d === 'string' ? d : d.name + if (!name) return + allLegends.add(name) + }) + Object.entries(selected ?? {}).forEach(([name, val]) => result[name] = val) + }) + allLegends.forEach(l => result[l] ?? (result[l] = true)) + return result +} + +const handleClick = (inst: ECharts, ev: ECElementEvent) => { + const { type, seriesName } = ev + if (type !== 'click' || !seriesName) return + + const option = inst.getOption() as EcOption + const currSelected = extractLegendSelected(option.legend) + const isOnlyCurrSelected = !!currSelected[seriesName] && Object.values(currSelected).filter(r => !!r).length === 1 + const seriesNames2Toggle = isOnlyCurrSelected + ? Object.keys(currSelected).filter(k => k !== seriesName) + : Object.entries(currSelected).filter(([k, v]) => k !== seriesName && !!v).map(([k]) => k) + seriesNames2Toggle.forEach(name => inst.dispatchAction({ type: "legendToggleSelect", name })) +} + const TimelineChart = defineComponent<{ data: timer.timeline.Tick[] }>(props => { const [myData] = useShadow(() => props.data) const { merge, setMerge, activities, dates } = useMerge(myData) @@ -33,7 +64,13 @@ const TimelineChart = defineComponent<{ data: timer.timeline.Tick[] }>(props => dates, })) - const { elRef } = useEcharts(Wrapper, bizData) + const { elRef } = useEcharts(Wrapper, bizData, { + afterInit: ew => { + const inst = ew.instance + if (!inst) return + inst.on('click', ev => handleClick(inst, ev)) + } + }) return () => ( From da7b12740496b0e4cf0bddba68f117d2835f2a01 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 26 Aug 2025 17:50:04 +0800 Subject: [PATCH 041/298] fix: fix score of timeline --- src/i18n/message/app/dashboard-resource.json | 4 +- .../components/Timeline/Chart/Wrapper.ts | 16 +++++ .../Dashboard/components/Timeline/Summary.tsx | 68 +++++++++---------- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json index 1edea729a..13e7e3278 100644 --- a/src/i18n/message/app/dashboard-resource.json +++ b/src/i18n/message/app/dashboard-resource.json @@ -61,9 +61,9 @@ }, "timeline": { "title": "Timeline of the recent {n} days", - "busyScore": "Busy Score", + "busyScore": "Busyness", "busyScoreDesc": "Related to the total browsing time and the number of websites per hour. See the source code for calculation formula", - "focusScore": "Focus Score", + "focusScore": "Focusness", "focusScoreDesc": "Related to the total time of continuous browsing of the same website. See the source code for calculation formula" } }, diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts index 858f5b3b1..0df34d624 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/Wrapper.ts @@ -144,6 +144,18 @@ const generateSeries = (biz: BizData, legendColors: Record): EcO }) } +const calcDataZoomDefaultRange = (activities: Activity[]): [start: number | undefined, end: number | undefined] => { + if (!activities.length) return [undefined, undefined] + let min = activities.map(a => a.start).reduce((a, b) => b < a ? b : a) + let max = activities.map(({ start, duration }) => start + duration).reduce((a, b) => b > a ? b : a) + + const interval = 30 * MILL_PER_MINUTE + min = Math.floor(min / interval) * interval + max = Math.ceil(max / interval) * interval + + return [min, max] +} + class Wrapper extends EchartsWrapper { protected replaceSeries: boolean = true @@ -163,6 +175,8 @@ class Wrapper extends EchartsWrapper { return name ? `${name} (${key})` : key } + const [zoomStart, zoomEnd] = calcDataZoomDefaultRange(bizData.activities) + return { grid: { left: gridLeft, width: domWidth - gridLeft - LEGEND_WIDTH, @@ -175,6 +189,8 @@ class Wrapper extends EchartsWrapper { height: 20, labelFormatter: '', handleStyle: { opacity: 0 }, + startValue: zoomStart, + endValue: zoomEnd, }, yAxis: { type: 'category', diff --git a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx index af5196edb..e5b47e5ca 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx +++ b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx @@ -1,31 +1,24 @@ import { t } from '@app/locale' import { InfoFilled } from '@element-plus/icons-vue' +import { useShadow } from '@hooks/index' import Flex from '@pages/components/Flex' import { groupBy } from '@util/array' import { MILL_PER_HOUR, MILL_PER_MINUTE } from '@util/time' import { ElIcon, ElRate, ElText, ElTooltip } from 'element-plus' -import { computed, defineComponent, FunctionalComponent } from 'vue' - -type ScoreValue = 1 | 2 | 3 | 4 | 5 +import { computed, defineComponent, watch } from 'vue' type AnalysisResult = { - busy: ScoreValue - focus: ScoreValue + busy: number + focus: number } +const MAX_SCORE = 5 + const defaultResult = (): AnalysisResult => ({ busy: 1, focus: 1, }) -const cvtRaw2Score = (raw: number): ScoreValue => { - if (raw < 1.5) return 1 - else if (raw < 2.5) return 2 - else if (raw < 3.5) return 3 - else if (raw < 4.5) return 4 - else return 5 -} - const computeSessionScore = (ticks: timer.timeline.Tick[], hourCount: number) => { let continuousSessions = 0 let currentSession: timer.timeline.Tick[] = [] @@ -50,7 +43,7 @@ const computeSessionScore = (ticks: timer.timeline.Tick[], hourCount: number) => if (currentSession.length > 1) continuousSessions++ const sessionDensity = continuousSessions / Math.max(1, hourCount / 10) - return Math.min(sessionDensity, 1) + return Math.min(sessionDensity, MAX_SCORE) } const analyze = (ticks: timer.timeline.Tick[]): AnalysisResult => { @@ -69,39 +62,42 @@ const analyze = (ticks: timer.timeline.Tick[]): AnalysisResult => { // busyScore = timeDensity * 0.6 + hostCountPerHour * 0.4 const timeDensity = totalActiveTime / totalRange + const timeDensityScore = Math.min(timeDensity / 0.3, MAX_SCORE) const maxHostCount = Object.values(hourlyData).map(hosts => hosts.size).sort((a, b) => b - a)[0]! - const busyRawScore = (timeDensity * 0.6 + (Math.min(maxHostCount / 10, 1) * 0.4)) * 5 + const hostMaxScore = Math.min(maxHostCount / 4, MAX_SCORE) + const busy = timeDensityScore * 0.6 + hostMaxScore * 0.4 // focusScore = duration * 0.7 + session * 0.3 const avgDuration = totalActiveTime / ticks.length - const avgDurationScore = Math.min(avgDuration / (2 * MILL_PER_MINUTE), 1) + const avgDurationScore = Math.min(avgDuration / (2 * MILL_PER_MINUTE), MAX_SCORE) const sessionScore = computeSessionScore(ticks, Object.keys(hourlyData).length) - const focusRawScore = (avgDurationScore * 0.7 + sessionScore * 0.3) * 5 + const focus = avgDurationScore * 0.7 + sessionScore * 0.3 - return { - busy: cvtRaw2Score(busyRawScore), - focus: cvtRaw2Score(focusRawScore), - } + return { busy, focus } } -const Score: FunctionalComponent<{ score: ScoreValue, label: string, desc: string }> = ({ score, label, desc }) => ( - - - {`${label} `} - - - - - - - - - - -) +const Score = defineComponent<{ score: number, label: string, desc: string }>(props => { + const [score] = useShadow(() => props.score) + return () => ( + + + {`${props.label} `} + + + + + + + + + + + ) +}, { props: ['desc', 'label', 'score'] }) const Summary = defineComponent<{ data: timer.timeline.Tick[] }>(props => { const ticks = computed(() => analyze(props.data)) + watch([ticks], () => console.log(ticks.value)) return () => ( From 5d53deb2a45707abe5c6bb688d92ee352665e36a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 26 Aug 2025 17:53:41 +0800 Subject: [PATCH 042/298] v3.6.2 --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 569393c66..f909654a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.2] - 2025-08-26 + +- Fixed some bugs of timeline + ## [3.6.1] - 2025-08-24 - Supported tracking timeline (#538) diff --git a/package.json b/package.json index 77549eaaa..4cf1daaa0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.1", + "version": "3.6.2", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -82,4 +82,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} From 9398dd015c6bb40ee90d6cd62b49d3ba4e466312 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 31 Aug 2025 16:03:22 +0800 Subject: [PATCH 043/298] feat: display restricted URL when page blocked --- .../limit/modal/components/Reason.tsx | 27 +++++++++++-------- src/content-script/limit/modal/context.ts | 11 ++++++++ src/content-script/limit/modal/index.ts | 3 ++- src/util/limit.ts | 12 ++++++--- test/util/limit.test.ts | 12 ++++++++- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/content-script/limit/modal/components/Reason.tsx b/src/content-script/limit/modal/components/Reason.tsx index 391fa8c67..3f905d562 100644 --- a/src/content-script/limit/modal/components/Reason.tsx +++ b/src/content-script/limit/modal/components/Reason.tsx @@ -1,11 +1,20 @@ import { t } from "@cs/locale" import { useRequest } from "@hooks/useRequest" import Flex from "@pages/components/Flex" -import { meetLimit, meetTimeLimit, period2Str } from "@util/limit" +import { matchCond, meetLimit, meetTimeLimit, period2Str } from "@util/limit" import { formatPeriodCommon, MILL_PER_SECOND } from "@util/time" import { ElDescriptions, ElDescriptionsItem, ElTag } from "element-plus" import { computed, defineComponent } from "vue" -import { useReason, useRule } from "../context" +import { useGlobalParam, useReason, useRule } from "../context" + +const renderBaseItems = (rule: timer.limit.Rule | null, url: string) => <> + msg.limit.item.name)} labelAlign="right"> + {rule?.name ?? '-'} + + msg.limit.item.condition)} labelAlign='right'> + {matchCond(rule?.cond ?? [], url)?.join(', ')} + + const TimeDescriptions = defineComponent({ props: { @@ -21,15 +30,14 @@ const TimeDescriptions = defineComponent({ setup(props) { const rule = useRule() const reason = useReason() + const { url } = useGlobalParam() const timeLimited = computed(() => meetTimeLimit(props.time ?? 0, props.waste ?? 0, !!reason.value?.allowDelay, reason.value?.delayCount ?? 0)) const visitLimited = computed(() => meetLimit(props.count ?? 0, props.visit ?? 0)) return () => ( - msg.limit.item.name)} labelAlign="right"> - {rule.value?.name ?? '-'} - + {renderBaseItems(rule.value, url)} {formatPeriodCommon((props.time ?? 0) * MILL_PER_SECOND)} @@ -67,6 +75,7 @@ const TimeDescriptions = defineComponent({ const _default = defineComponent(() => { const reason = useReason() const rule = useRule() + const { url } = useGlobalParam() const type = computed(() => reason.value?.type) const { data: browsingTime, refresh: refreshBrowsingTime } = useRequest(() => { @@ -98,9 +107,7 @@ const _default = defineComponent(() => { dataLabel={t(msg => msg.calendar.range.thisWeek)} /> - msg.limit.item.name)} labelAlign="right"> - {rule.value?.name || '-'} - + {renderBaseItems(rule.value, url)} msg.limit.item.visitTime)} labelAlign="right"> {formatPeriodCommon((rule.value?.visitTime ?? 0) * MILL_PER_SECOND) || '-'} @@ -114,9 +121,7 @@ const _default = defineComponent(() => { - msg.limit.item.name)} labelAlign="right"> - {rule.value?.name || '-'} - + {renderBaseItems(rule.value, url)} msg.limit.item.period)} labelAlign="right"> { rule.value?.periods?.length diff --git a/src/content-script/limit/modal/context.ts b/src/content-script/limit/modal/context.ts index 5af5803ad..e45b2ec01 100644 --- a/src/content-script/limit/modal/context.ts +++ b/src/content-script/limit/modal/context.ts @@ -6,8 +6,19 @@ import { type LimitReason } from "../common" const REASON_KEY = "display_reason" const RULE_KEY = "display_rule" +const GLOBAL_KEY = "delay_global" const DELAY_HANDLER_KEY = 'delay_handler' +type GlobalParam = { + url: string +} + +export const provideGlobalParam = (app: App, gp: GlobalParam) => { + app.provide(GLOBAL_KEY, gp) +} + +export const useGlobalParam = () => inject(GLOBAL_KEY) as GlobalParam + export const provideReason = (app: App): Ref => { const reason = ref() app.provide(REASON_KEY, reason) diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index b0c2ed8e5..a6534c3f9 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -5,7 +5,7 @@ import { createApp, Ref, type App } from 'vue' import { exitFullscreen, isSameReason, type LimitReason, type MaskModal } from '../common' import { TAG_NAME, type RootElement } from '../element' import Main from './Main' -import { provideDelayHandler, provideReason } from './context' +import { provideDelayHandler, provideGlobalParam, provideReason } from './context' function pauseAllVideo(): void { const elements = document?.getElementsByTagName('video') @@ -156,6 +156,7 @@ class ModalInstance implements MaskModal { private initApp() { this.app = createApp(Main) this.reason = provideReason(this.app) + provideGlobalParam(this.app, { url: this.url }) provideDelayHandler(this.app, () => this.delayHandlers?.forEach(h => h?.())) this.body && this.app.mount(this.body) } diff --git a/src/util/limit.ts b/src/util/limit.ts index 546cb53dc..d902fbb6c 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -14,10 +14,16 @@ export const cleanCond = (origin: string | undefined): string | undefined => { return res || undefined } +const matchUrl = (cond: string, url: string): boolean => { + return new RegExp(`^.*//${cond.split('*').join('.*')}`).test(url) +} + export function matches(cond: timer.limit.Item['cond'], url: string): boolean { - return cond?.some?.( - c => new RegExp(`^.*//${(c || '').split('*').join('.*')}`).test(url) - ) + return cond.some(c => matchUrl(c, url)) +} + +export function matchCond(cond: timer.limit.Item['cond'], url: string): string[] { + return cond.filter(c => matchUrl(c, url)) } export const meetLimit = (limit: number | undefined, value: number | undefined): boolean => { diff --git a/test/util/limit.test.ts b/test/util/limit.test.ts index d50d69aba..2dabc6f0d 100644 --- a/test/util/limit.test.ts +++ b/test/util/limit.test.ts @@ -1,4 +1,7 @@ -import { calcTimeState, cleanCond, dateMinute2Idx, hasLimited, hasWeeklyLimited, isEffective, isEnabledAndEffective, matches, meetLimit, meetTimeLimit, period2Str } from "@util/limit" +import { + calcTimeState, cleanCond, dateMinute2Idx, hasLimited, hasWeeklyLimited, isEffective, isEnabledAndEffective, + matchCond, matches, meetLimit, meetTimeLimit, period2Str +} from "@util/limit" describe('util/limit', () => { test('cleanCond', () => { @@ -18,6 +21,13 @@ describe('util/limit', () => { expect(matches(cond, 'http://github.com')).toBe(false) }) + test('matchCond', () => { + const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh', 'github.com'] + expect(matchCond(cond, 'http://www.baidu.com')).toEqual(['www.baidu.com']) + expect(matchCond(cond, 'https://github.com/sheepzh/time-tracker-for-browser')).toEqual(['github.com/sheepzh', 'github.com']) + expect(matchCond(cond, 'https://www.github.com')).toEqual([]) + }) + test('meetLimit', () => { expect(meetLimit(undefined, undefined)).toBe(false) expect(meetLimit(1, undefined)).toBe(false) From 5c329b2614633ebd9dba317dee6b6ec30e83a436 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:00:16 +0800 Subject: [PATCH 044/298] chore(psl): update PSL list by bot (#542) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/util/psl/rules.json | 80 ++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/src/util/psl/rules.json b/src/util/psl/rules.json index 601f53368..14e97c0c4 100644 --- a/src/util/psl/rules.json +++ b/src/util/psl/rules.json @@ -170,10 +170,8 @@ "agency": 1, "ai": { "c": { - "caffeine": 1, "com": 1, "framer": 1, - "id": 1, "net": 1, "off": 1, "org": 1, @@ -325,6 +323,11 @@ "nyat": 1, "on-fleek": 1, "ondigitalocean": 1, + "railway": { + "c": { + "up": 1 + } + }, "replit": { "c": { "id": 1 @@ -361,6 +364,7 @@ } }, "vercel": 1, + "wal": 1, "wdh": 1, "web": 1, "windsurf": 1, @@ -514,12 +518,7 @@ "c": { "act": 1, "catholic": 1, - "nsw": { - "c": { - "schools": 1 - }, - "l": 1 - }, + "nsw": 1, "nt": 1, "qld": 1, "sa": 1, @@ -539,6 +538,12 @@ }, "l": 1 }, + "hrsn": { + "c": { + "vps": 1 + }, + "l": 1 + }, "id": 1, "net": 1, "nsw": 1, @@ -1444,6 +1449,7 @@ "am": 1, "anani": 1, "aparecida": 1, + "api": 1, "app": 1, "arq": 1, "art": 1, @@ -1537,6 +1543,7 @@ "l": 1 }, "gru": 1, + "ia": 1, "imb": 1, "ind": 1, "inf": 1, @@ -1632,6 +1639,7 @@ "sjc": 1, "slg": 1, "slz": 1, + "social": 1, "sorocaba": 1, "srv": 1, "taxi": 1, @@ -1649,6 +1657,7 @@ "vix": 1, "vlog": 1, "wiki": 1, + "xyz": 1, "zlg": 1 }, "l": 1 @@ -1996,6 +2005,7 @@ "it1": 1 } }, + "jote": 1, "jotelulu": 1, "keliweb": { "c": { @@ -2371,6 +2381,7 @@ "l": 1 }, "gov": 1, + "hidns": 1, "leadpages": 1, "lpages": 1, "mil": 1, @@ -2389,7 +2400,13 @@ }, "l": 1 }, - "supabase": 1, + "supabase": { + "c": { + "realtime": 1, + "storage": 1 + }, + "l": 1 + }, "xmit": { "c": { "*": 1 @@ -4218,6 +4235,8 @@ } }, "on-aptible": 1, + "on-forge": 1, + "on-vapor": 1, "onfabrica": 1, "onrender": 1, "onthewifi": 1, @@ -4269,6 +4288,16 @@ "qa2": 1, "qbuser": 1, "qualifioapp": 1, + "qualyhqpartner": { + "c": { + "*": 1 + } + }, + "qualyhqportal": { + "c": { + "*": 1 + } + }, "quicksytes": 1, "quipelements": { "c": { @@ -4312,6 +4341,8 @@ } } }, + "same-app": 1, + "same-preview": 1, "saves-the-whales": 1, "scrysec": 1, "securitytactics": 1, @@ -4575,6 +4606,7 @@ } }, "e4": 1, + "gov": 1, "metacentrum": { "c": { "cloud": { @@ -4616,6 +4648,7 @@ "4lima": 1, "barsy": 1, "bplaced": 1, + "co": 1, "com": 1, "community-pro": 1, "cosidns": { @@ -4799,6 +4832,12 @@ "deno": 1, "deno-staging": 1, "deta": 1, + "erp": { + "c": { + "web": 1 + }, + "l": 1 + }, "evervault": { "c": { "relay": 1 @@ -5498,7 +5537,6 @@ "giving": 1, "gl": { "c": { - "biz": 1, "co": 1, "com": 1, "edu": 1, @@ -6064,7 +6102,6 @@ "hostyhosting": 1, "hypernode": 1, "hzc": 1, - "icp-api": 1, "icp0": { "c": { "raw": { @@ -9391,7 +9428,6 @@ "edu": 1, "filegear": 1, "filegear-sg": 1, - "glitch": 1, "gov": 1, "hopto": 1, "i234": 1, @@ -9889,6 +9925,11 @@ "homelinux": 1, "homeunix": 1, "hu": 1, + "icp": { + "c": { + "*": 1 + } + }, "in": 1, "in-dsl": 1, "in-the-band": 1, @@ -10064,7 +10105,8 @@ }, "l": 1 }, - "za": 1 + "za": 1, + "zabc": 1 }, "l": 1 }, @@ -11994,7 +12036,6 @@ "realty": 1, "recipes": 1, "red": 1, - "redstone": 1, "redumbrella": 1, "rehab": 1, "reise": 1, @@ -12466,6 +12507,7 @@ } }, "preview": 1, + "sourcecraft": 1, "square": 1, "srht": 1, "tst": { @@ -13293,8 +13335,7 @@ }, "de": { "c": { - "cc": 1, - "lib": 1 + "cc": 1 }, "l": 1 }, @@ -13797,7 +13838,12 @@ "viking": 1, "villas": 1, "vin": 1, - "vip": 1, + "vip": { + "c": { + "hidns": 1 + }, + "l": 1 + }, "virgin": 1, "visa": 1, "vision": 1, From 8f036df098338cbc3efb33d66ee055bd36ab2671 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 5 Sep 2025 14:02:00 +0800 Subject: [PATCH 045/298] test: fix tests --- test-e2e/limit/daily-time.test.ts | 2 +- test-e2e/limit/daily-visit.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test-e2e/limit/daily-time.test.ts b/test-e2e/limit/daily-time.test.ts index 37c7ffd4c..a7d6513ff 100644 --- a/test-e2e/limit/daily-time.test.ts +++ b/test-e2e/limit/daily-time.test.ts @@ -46,7 +46,7 @@ describe('Daily time limit', () => { const descEl = shadow?.shadowRoot?.querySelector('#app .el-descriptions:not([style*="display: none"])') const trs = descEl?.querySelectorAll('tr') const name = trs?.[0]?.querySelector('td:nth-child(2)')?.textContent - const timeStr = trs?.[2]?.querySelector('td:nth-child(2) .el-tag--danger')?.textContent + const timeStr = trs?.[3]?.querySelector('td:nth-child(2) .el-tag--danger')?.textContent return { name, time: parseInt(timeStr?.replace('s', '').trim() ?? '0') } }) expect(name).toEqual(demoRule.name) diff --git a/test-e2e/limit/daily-visit.test.ts b/test-e2e/limit/daily-visit.test.ts index a4280fb99..f7a33b1e1 100644 --- a/test-e2e/limit/daily-visit.test.ts +++ b/test-e2e/limit/daily-visit.test.ts @@ -42,7 +42,7 @@ describe('Daily time limit', () => { const descEl = shadow!.shadowRoot!.querySelector('#app .el-descriptions:not([style*="display: none"])') const trs = descEl!.querySelectorAll('tr') const name = trs[0].querySelector('td:nth-child(2)')!.textContent - const count = trs[2].querySelector('td:nth-child(2) .el-tag--danger')!.textContent + const count = trs[3].querySelector('td:nth-child(2) .el-tag--danger')!.textContent return { name, count } }) From cbe6daa6150afeb58b8754b674af02b9058bb3ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:32:54 +0800 Subject: [PATCH 046/298] build(deps): bump element-plus from 2.11.1 to 2.11.2 (#546) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.11.1 to 2.11.2. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.11.1...2.11.2) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.11.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4cf1daaa0..41fc08144 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@vueuse/core": "^13.7.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.11.1", + "element-plus": "2.11.2", "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From 92e5ad50f5b400f87d99ee824ae4a67717bd990e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:20:13 +0800 Subject: [PATCH 047/298] build(deps-dev): bump @types/chrome from 0.1.4 to 0.1.9 (#547) Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.4 to 0.1.9. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome) --- updated-dependencies: - dependency-name: "@types/chrome" dependency-version: 0.1.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41fc08144..1466396c2 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rspack/core": "^1.4.11", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.4", + "@types/chrome": "0.1.9", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.3.0", From 01db53a2d03d05a1c15006225bce424075478533 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 11 Sep 2025 18:55:15 +0800 Subject: [PATCH 048/298] feat: slide date range (#530) --- package.json | 24 +++--- .../common/filter/DateRangeFilterItem.tsx | 75 ++++++++++--------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 1466396c2..c1fc2612d 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,16 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.3", - "@crowdin/crowdin-api-client": "^1.46.0", + "@crowdin/crowdin-api-client": "^1.46.2", "@rsdoctor/rspack-plugin": "^1.2.3", - "@rspack/cli": "^1.4.11", - "@rspack/core": "^1.4.11", + "@rspack/cli": "^1.5.3", + "@rspack/core": "^1.5.3", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.9", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.3.0", + "@types/node": "^24.3.1", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", @@ -48,14 +48,14 @@ "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", - "jest": "^30.0.5", - "jest-environment-jsdom": "^30.0.5", + "jest": "^30.1.3", + "jest-environment-jsdom": "^30.1.2", "jest-junit": "^16.0.0", "postcss": "^8.5.6", - "postcss-loader": "^8.1.1", + "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.17.0", - "sass": "^1.90.0", + "puppeteer": "^24.20.0", + "sass": "^1.92.1", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "ts-loader": "^9.5.4", @@ -69,17 +69,17 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", - "@vueuse/core": "^13.7.0", + "@vueuse/core": "^13.9.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", "element-plus": "2.11.2", "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", - "vue": "^3.5.19", + "vue": "^3.5.21", "vue-router": "^4.5.1" }, "engines": { "node": ">=20" } -} +} \ No newline at end of file diff --git a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx index 47e30b478..28d2d7f9c 100644 --- a/src/pages/app/components/common/filter/DateRangeFilterItem.tsx +++ b/src/pages/app/components/common/filter/DateRangeFilterItem.tsx @@ -35,57 +35,58 @@ const ARROW_BTN_STYLE: StyleValue = { padding: '8px 1px', } -const DateRangeFilterItem = defineComponent(props => { - const rtl = isRtl() - - const backwardDate = computed(() => { - const start = props.modelValue?.[0] - if (!start) return undefined - const time = start.getTime() - return new Date(time - MILL_PER_DAY) - }) +const useRange = (props: Props) => { const backwardDisabled = computed(() => { - if (!backwardDate.value) return true + const start = props.modelValue?.[0] + if (!start) return true const { disabledDate } = props if (!disabledDate) return false - return disabledDate(backwardDate.value) - }) - const forwardDate = computed(() => { - const end = props.modelValue?.[1] - if (!end) return undefined - const time = end.getTime() - return new Date(time + MILL_PER_DAY) + const lastDay = new Date(start.getTime() - MILL_PER_DAY) + return disabledDate(lastDay) }) + const forwardDisabled = computed(() => { - if (!forwardDate.value) return true + const end = props.modelValue?.[1] + if (!end) return true const { disabledDate } = props if (!disabledDate) return false - return disabledDate(forwardDate.value) + const nextDate = new Date(end.getTime() + MILL_PER_DAY) + return disabledDate(nextDate) }) - const handleChange = (newVal: [Date, Date] | undefined) => { - const [start, end] = newVal || [] - const isClearChosen = !start?.getTime?.() && !end?.getTime?.() - if (isClearChosen) newVal = undefined - props.onChange(newVal) + const shift = (dayNum: number) => { + const { modelValue, onChange } = props + const [start, end] = modelValue ?? [] + if (!start || !end) return + const millDiff = MILL_PER_DAY * dayNum + const newStart = new Date(start.getTime() + millDiff) + const newEnd = new Date(end.getTime() + millDiff) + onChange?.([newStart, newEnd]) } const clearable = toRef(props, "clearable", true) - const shortcuts = () => { + const shortcuts = computed(() => { const { shortcuts: value } = props if (!value?.length || !clearable.value) return value return [...value, clearShortcut()] - } + }) - const backward = () => { - const { modelValue, onChange } = props - backwardDate.value && modelValue && onChange([backwardDate.value, modelValue[1]]) - } - const forward = () => { - const { modelValue, onChange } = props - forwardDate.value && modelValue && onChange([modelValue[0], forwardDate.value]) + return { + backwardDisabled, forwardDisabled, + shift, + clearable, shortcuts, } +} + +const DateRangeFilterItem = defineComponent(props => { + const rtl = isRtl() + const { + backwardDisabled, + forwardDisabled, + shift, + shortcuts, clearable, + } = useRange(props) return () => ( @@ -93,7 +94,7 @@ const DateRangeFilterItem = defineComponent(props => { shift(-1)} style={{ ...ARROW_BTN_STYLE, ...rtl ? { @@ -111,8 +112,8 @@ const DateRangeFilterItem = defineComponent(props => { type="daterange" rangeSeparator="-" disabledDate={props.disabledDate} - shortcuts={shortcuts()} - onUpdate:modelValue={newVal => handleChange(toRaw(newVal))} + shortcuts={shortcuts.value} + onUpdate:modelValue={newVal => props.onChange?.(toRaw(newVal) ?? undefined)} startPlaceholder={props.startPlaceholder} endPlaceholder={props.endPlaceholder} clearable={clearable.value} @@ -125,7 +126,7 @@ const DateRangeFilterItem = defineComponent(props => { shift(1)} style={{ ...ARROW_BTN_STYLE, ...rtl ? { From 79b96f99d9e20b54688b8a80a79e3b81fff2ff7a Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 11 Sep 2025 19:00:16 +0800 Subject: [PATCH 049/298] v3.6.3 --- CHANGELOG.md | 5 +++++ package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f909654a9..a0551f417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.3] - 2025-09-11 + +- Optimized the block page +- Optimized the date range filter + ## [3.6.2] - 2025-08-26 - Fixed some bugs of timeline diff --git a/package.json b/package.json index c1fc2612d..84e3f2f3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.2", + "version": "3.6.3", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -82,4 +82,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} From 1ad3f35617c7396553a2ac36c7a2a10ee94bc7fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:58:18 +0800 Subject: [PATCH 050/298] build(deps-dev): bump @types/chrome from 0.1.9 to 0.1.11 (#549) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84e3f2f3d..1f0907e31 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rspack/core": "^1.5.3", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.9", + "@types/chrome": "0.1.11", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.3.1", From bb93b775f2f54f447560531fe75d934c827f94e2 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 17 Sep 2025 10:14:08 +0800 Subject: [PATCH 051/298] fix: use gray color for other items (#541) --- package.json | 12 ++++++------ src/pages/popup/components/Percentage/chart.ts | 15 +++++++-------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 1f0907e31..2e4a8ae33 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,16 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.3", - "@crowdin/crowdin-api-client": "^1.46.2", + "@crowdin/crowdin-api-client": "^1.47.1", "@rsdoctor/rspack-plugin": "^1.2.3", - "@rspack/cli": "^1.5.3", - "@rspack/core": "^1.5.3", + "@rspack/cli": "^1.5.4", + "@rspack/core": "^1.5.4", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.11", + "@types/chrome": "0.1.12", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.3.1", + "@types/node": "^24.5.1", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", @@ -54,7 +54,7 @@ "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.20.0", + "puppeteer": "^24.21.0", "sass": "^1.92.1", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", diff --git a/src/pages/popup/components/Percentage/chart.ts b/src/pages/popup/components/Percentage/chart.ts index b27ac1d78..672ce0dfe 100644 --- a/src/pages/popup/components/Percentage/chart.ts +++ b/src/pages/popup/components/Percentage/chart.ts @@ -1,5 +1,5 @@ import { createTab } from "@api/chrome/tab" -import { getCssVariable, getPrimaryTextColor, getSecondaryTextColor } from "@pages/util/style" +import { getCssVariable, getInfoColor, getPrimaryTextColor, getSecondaryTextColor } from "@pages/util/style" import { calJumpUrl } from "@popup/common" import { t } from "@popup/locale" import { sum, toMap } from "@util/array" @@ -204,23 +204,22 @@ export function generateSiteSeriesOption(rows: timer.stat.Row[], result: Percent const chartRows = cvt2ChartRows(rows, dimension, itemCount) const iconRich: PieLabelRichOption = {} const data = chartRows.map(row => { - const value = row[dimension] - let name = 'NaN' + const item: PieSeriesItemOption = { name: 'NaN', value: row[dimension], row } if (isOther(row)) { - const { count } = row - name = t(msg => msg.content.percentage.otherLabel, { count }) + item.itemStyle = { color: getInfoColor() } + item.name = t(msg => msg.content.percentage.otherLabel, { count: row.count }) } else if (isSite(row)) { const { siteKey, alias, iconUrl } = row const { host } = siteKey || {} - name = (displaySiteName ? (alias ?? host) : host) ?? '' + const name = item.name = (displaySiteName ? (alias ?? host) : host) ?? '' const richValue: PieLabelRichValueOption = { ...BASE_LABEL_RICH_VALUE } iconUrl && (richValue.backgroundColor = { image: iconUrl }) iconRich[legend2LabelStyle(name)] = richValue } else if (isGroup(row)) { - name = getGroupName(groupMap, row) + item.name = getGroupName(groupMap, row) } - return { name, value, row } satisfies PieSeriesItemOption + return item }) const textColor = getPrimaryTextColor() From 007db6713b4e451b55c485df8e502a46e3d23a2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 01:39:42 +0800 Subject: [PATCH 052/298] build(deps): bump element-plus from 2.11.2 to 2.11.3 (#550) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.11.2 to 2.11.3. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.11.2...2.11.3) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e4a8ae33..4435cea5a 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@vueuse/core": "^13.9.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.11.2", + "element-plus": "2.11.3", "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From f6aa7b5aa4b6bcba2f46d5dde88ccc73f3ff1861 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 20 Sep 2025 10:22:55 +0800 Subject: [PATCH 053/298] v3.6.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4435cea5a..ef0e7c45c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.3", + "version": "3.6.4", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From 1084bb2b0ba2b1654a6591879f021e3fdfe32eac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:12:36 +0800 Subject: [PATCH 054/298] build(deps): bump element-plus from 2.11.3 to 2.11.4 (#552) Bumps [element-plus](https://github.com/element-plus/element-plus) from 2.11.3 to 2.11.4. - [Release notes](https://github.com/element-plus/element-plus/releases) - [Changelog](https://github.com/element-plus/element-plus/blob/dev/CHANGELOG.en-US.md) - [Commits](https://github.com/element-plus/element-plus/compare/2.11.3...2.11.4) --- updated-dependencies: - dependency-name: element-plus dependency-version: 2.11.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef0e7c45c..424b15007 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@vueuse/core": "^13.9.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", - "element-plus": "2.11.3", + "element-plus": "2.11.4", "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", From b45e3d09814c17e68e379716eb7e5bdf742f8fbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:13:10 +0800 Subject: [PATCH 055/298] build(deps-dev): bump commitlint from 19.8.1 to 20.0.0 (#551) Bumps [commitlint](https://github.com/conventional-changelog/commitlint/tree/HEAD/@alias/commitlint) from 19.8.1 to 20.0.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@alias/commitlint/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.0.0/@alias/commitlint) --- updated-dependencies: - dependency-name: commitlint dependency-version: 20.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 424b15007..dbb7440ab 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", - "commitlint": "^19.8.1", + "commitlint": "^20.0.0", "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", From 2e9bd6daf8da6427637322b1f7a30b8aec860e7e Mon Sep 17 00:00:00 2001 From: sheepie Date: Sun, 28 Sep 2025 13:39:28 +0800 Subject: [PATCH 056/298] chore: add initial steps --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e152cfc8..8e503a369 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,11 @@ Some free open source tools are also integrated: ## 2. Steps 1. Fork your repository -2. Install dependencies +2. Install dependencies and initialize ```shell npm install +npm run prepare ``` 3. Create your own branch From 61dbeb252ae9e6c3ea893ac131cf5db0ab1e834b Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 28 Sep 2025 19:13:06 +0800 Subject: [PATCH 057/298] fix: remove map files while building --- rspack/rspack.prod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rspack/rspack.prod.ts b/rspack/rspack.prod.ts index 90d990e49..139f0a555 100644 --- a/rspack/rspack.prod.ts +++ b/rspack/rspack.prod.ts @@ -36,6 +36,6 @@ const option = optionGenerator({ outputPath, manifest, mode: "production" }) const { plugins = [] } = option plugins.push(filemanagerPlugin) option.plugins = plugins -// option.devtool = false +option.devtool = false export default option \ No newline at end of file From ed4d9ae24202cdb5d3051d9575e68da6f63d2921 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 29 Sep 2025 19:20:54 +0800 Subject: [PATCH 058/298] build: upgrade deps --- package.json | 22 +++++++++++----------- types/styles.d.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 types/styles.d.ts diff --git a/package.json b/package.json index dbb7440ab..5470035a3 100644 --- a/package.json +++ b/package.json @@ -31,16 +31,16 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.3", - "@crowdin/crowdin-api-client": "^1.47.1", - "@rsdoctor/rspack-plugin": "^1.2.3", - "@rspack/cli": "^1.5.4", - "@rspack/core": "^1.5.4", + "@crowdin/crowdin-api-client": "^1.48.3", + "@rsdoctor/rspack-plugin": "^1.3.1", + "@rspack/cli": "^1.5.8", + "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", "@types/chrome": "0.1.12", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.5.1", + "@types/node": "^24.5.2", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", @@ -48,14 +48,14 @@ "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", - "jest": "^30.1.3", - "jest-environment-jsdom": "^30.1.2", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", "jest-junit": "^16.0.0", "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.21.0", - "sass": "^1.92.1", + "puppeteer": "^24.22.3", + "sass": "^1.93.2", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", "ts-loader": "^9.5.4", @@ -65,7 +65,7 @@ "url-loader": "^4.1.1" }, "optionalDependencies": { - "web-ext": "^8.9.0" + "web-ext": "^8.10.0" }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", @@ -76,7 +76,7 @@ "js-base64": "^3.7.8", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", - "vue": "^3.5.21", + "vue": "^3.5.22", "vue-router": "^4.5.1" }, "engines": { diff --git a/types/styles.d.ts b/types/styles.d.ts new file mode 100644 index 000000000..4d2aba51b --- /dev/null +++ b/types/styles.d.ts @@ -0,0 +1,10 @@ +declare module '*.css' { + const classes: { [key: string]: string } + export default classes +} + +declare module '*.sass' { + const classes: { [key: string]: string } + export default classes +} + From bbe5ed00c6da8935a227ce780138da4442abcab5 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Mon, 29 Sep 2025 19:21:10 +0800 Subject: [PATCH 059/298] refactor: remove unused console.log --- src/database/stat-database/filter.ts | 1 - .../components/Dashboard/components/Timeline/Summary.tsx | 3 +-- .../app/components/Habit/components/Site/TopK/Wrapper.ts | 1 - src/pages/app/components/Limit/context.ts | 6 +++--- .../app/components/Option/components/LimitOption/index.tsx | 6 +++--- src/pages/app/components/common/NumberGrow.tsx | 2 +- src/service/limit-service/index.ts | 2 +- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/database/stat-database/filter.ts b/src/database/stat-database/filter.ts index 00d8726fa..d91c894dc 100644 --- a/src/database/stat-database/filter.ts +++ b/src/database/stat-database/filter.ts @@ -122,7 +122,6 @@ export async function filter(this: StatDatabase, condition?: StatCondition, only const date = key.substring(0, 8) let host = key.substring(8) if (onlyGroup) { - // console.log(date, host,) if (host.startsWith(GROUP_PREFIX)) { host = host.substring(GROUP_PREFIX.length) result.push({ date, host, value: value as timer.core.Result }) diff --git a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx index e5b47e5ca..5cff96f6a 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx +++ b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx @@ -5,7 +5,7 @@ import Flex from '@pages/components/Flex' import { groupBy } from '@util/array' import { MILL_PER_HOUR, MILL_PER_MINUTE } from '@util/time' import { ElIcon, ElRate, ElText, ElTooltip } from 'element-plus' -import { computed, defineComponent, watch } from 'vue' +import { computed, defineComponent } from 'vue' type AnalysisResult = { busy: number @@ -97,7 +97,6 @@ const Score = defineComponent<{ score: number, label: string, desc: string }>(pr const Summary = defineComponent<{ data: timer.timeline.Tick[] }>(props => { const ticks = computed(() => analyze(props.data)) - watch([ticks], () => console.log(ticks.value)) return () => ( diff --git a/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts b/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts index f7f77af9c..85d981f7d 100644 --- a/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts +++ b/src/pages/app/components/Habit/components/Site/TopK/Wrapper.ts @@ -98,7 +98,6 @@ async function generateOption(rows: timer.stat.Row[] = [], timeFormat: timer.app title: generateTitleOption(title), grid: { left: `${MARGIN_LEFT_P}%`, - containLabel: true, right: `${MARGIN_RIGHT_P}%`, top: "16%", bottom: '4%', diff --git a/src/pages/app/components/Limit/context.ts b/src/pages/app/components/Limit/context.ts index 06822a393..2d9f34d1c 100644 --- a/src/pages/app/components/Limit/context.ts +++ b/src/pages/app/components/Limit/context.ts @@ -110,7 +110,7 @@ export const useLimitProvider = () => { row.enabled = enabled await limitService.updateEnabled(toRaw(row)) } catch (e) { - console.log(e) + console.warn(e) } } @@ -121,7 +121,7 @@ export const useLimitProvider = () => { row.allowDelay = delayable await limitService.updateDelay(toRaw(row)) } catch (e) { - console.log(e) + console.warn(e) } } @@ -137,7 +137,7 @@ export const useLimitProvider = () => { row.locked = locked await limitService.updateLocked(toRaw(row)) } catch (e) { - console.log(e) + console.warn(e) } } diff --git a/src/pages/app/components/Option/components/LimitOption/index.tsx b/src/pages/app/components/Option/components/LimitOption/index.tsx index 513ee03a4..91e814057 100644 --- a/src/pages/app/components/Option/components/LimitOption/index.tsx +++ b/src/pages/app/components/Option/components/LimitOption/index.tsx @@ -79,7 +79,7 @@ const _default = defineComponent((_, ctx) => { } option.limitLevel = val } catch (e) { - console.log("Failed to verify", e) + console.warn("Failed to verify", e) } } @@ -89,7 +89,7 @@ const _default = defineComponent((_, ctx) => { option.limitPassword = await modifyPsw() ElMessage.success(t(msg => msg.operation.successMsg)) } catch (e) { - console.log("Failed to verify", e) + console.warn("Failed to verify", e) } } @@ -154,7 +154,7 @@ const _default = defineComponent((_, ctx) => { size="small" onChange={(val: timer.limit.VerificationDifficulty) => verify() .then(() => option.limitVerifyDifficulty = val) - .catch(console.log) + .catch(console.warn) } > {ALL_DIFF.map(item => msg.option.dailyLimit.level.verificationDifficulty[item])} />)} diff --git a/src/pages/app/components/common/NumberGrow.tsx b/src/pages/app/components/common/NumberGrow.tsx index 55ea222a8..f87f092e4 100644 --- a/src/pages/app/components/common/NumberGrow.tsx +++ b/src/pages/app/components/common/NumberGrow.tsx @@ -28,7 +28,7 @@ const NumberGrow = defineComponent(props => { separator: getNumberSeparator(), }) if (countUp.value.error) { - console.log(countUp.value.error) + console.warn(countUp.value.error) } countUp.value.start() }) diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts index 2a42ce20e..435925bbc 100644 --- a/src/service/limit-service/index.ts +++ b/src/service/limit-service/index.ts @@ -67,7 +67,7 @@ async function noticeLimitChanged() { tabs.forEach(({ id, url }) => { if (!id || !url) return const limitedItems = effectiveItems.filter(item => matches(item?.cond, url)) - sendMsg2Tab(id, 'limitChanged', limitedItems).catch(err => console.log(err.message)) + sendMsg2Tab(id, 'limitChanged', limitedItems).catch(err => console.warn(err.message)) }) } From f78848879f902ec350fe4d3013eaea46430254af Mon Sep 17 00:00:00 2001 From: KHDK Date: Tue, 30 Sep 2025 14:00:39 +0800 Subject: [PATCH 060/298] feat: implement the requirement of excluding subpages for entries starting with '+' (#554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修改matches和matchCond函数以支持类似LeechBlock的排除方法,即以+开头的规则作为排除规则,以排除相应的子页面 * 修改测试文件 * 修改测试文件 * feat: implement the requirement of excluding subpages for entries starting with '+' * Revert "feat: implement the requirement of excluding subpages for entries starting with '+'" This reverts commit 411320bfa21a87f8af7db0501e3fe9eed3a4a160. * feat: implement the requirement of excluding subpages for entries starting with '+' * Revert "feat: implement the requirement of excluding subpages for entries starting with '+'" This reverts commit 404fb63286e5867f5257cba104ab333ee19dac43. * feat: implement the requirement of excluding subpages for entries starting with '+' * perf: use for loops for performance optimization * perf: reduce multiple for loops to a single one --- src/util/limit.ts | 34 ++++++++++++++++++++++++++++++++-- test/util/limit.test.ts | 16 +++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/util/limit.ts b/src/util/limit.ts index d902fbb6c..3809372fb 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -18,12 +18,42 @@ const matchUrl = (cond: string, url: string): boolean => { return new RegExp(`^.*//${cond.split('*').join('.*')}`).test(url) } +const WHITE_PREFIX = '+' //Denotes an exclusion rule when used as a prefix in a condition string + +/** + * checks whether the provided URL matches the rule list (cond), following the exclusion rule priority + * @param cond + * @param url + */ export function matches(cond: timer.limit.Item['cond'], url: string): boolean { - return cond.some(c => matchUrl(c, url)) + let hit = false + for (let i = cond.length - 1; i >= 0; i--) { + const rule = cond[i] + if (rule.startsWith(WHITE_PREFIX)) { + if (matchUrl(rule.slice(1), url)) return false + } else { + hit = hit || matchUrl(rule, url) + } + } + return hit } +/** + * determines which normal rules in the given list (cond) match the provided URL, strictly adhering to exclusion rule priority + * @param cond + * @param url + */ export function matchCond(cond: timer.limit.Item['cond'], url: string): string[] { - return cond.filter(c => matchUrl(c, url)) + const matchedNormalRules: string[] = []; + for (let i = cond.length - 1; i >= 0; i--) { + const rule = cond[i]; + if (rule.startsWith(WHITE_PREFIX)) { + if (matchUrl(rule.slice(1), url)) return []; //Immediately return an empty array if an exclusion rule is hit + } else { + if (matchUrl(rule, url)) matchedNormalRules.push(rule); + } + } + return matchedNormalRules; } export const meetLimit = (limit: number | undefined, value: number | undefined): boolean => { diff --git a/test/util/limit.test.ts b/test/util/limit.test.ts index 2dabc6f0d..4055013ea 100644 --- a/test/util/limit.test.ts +++ b/test/util/limit.test.ts @@ -13,19 +13,25 @@ describe('util/limit', () => { }) test('matches', () => { - const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh'] + const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh', '+github.com/sheepzh/timer','+www.bilibili.com/cheese','*.bilibili.com*'] expect(matches(cond, 'https://www.baidu.com')).toBe(true) expect(matches(cond, 'http://hk.google.com')).toBe(true) - expect(matches(cond, 'http://github.com/sheepzh/timer')).toBe(true) - expect(matches(cond, 'http://github.com')).toBe(false) + expect(matches(cond, 'http://github.com/sheepzh/poetry')).toBe(true) + expect(matches(cond, 'http://github.com/sheepzh/timer')).toBe(false) + expect(matches(cond, 'http://github.com/sheepzh/timer/test')).toBe(false) + expect(matches(cond, 'http://www.bilibili.com/cheese/list')).toBe(false) + expect(matches(cond, 'http://t.bilibili.com/')).toBe(true) + expect(matches(cond, 'https://www.bilibili.com/video/BV3527/')).toBe(true) }) test('matchCond', () => { - const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh', 'github.com'] + const cond = ['www.baidu.com', '*.google.com', 'github.com/sheepzh', 'github.com','+www.bilibili.com/cheese','*.bilibili.com*'] expect(matchCond(cond, 'http://www.baidu.com')).toEqual(['www.baidu.com']) - expect(matchCond(cond, 'https://github.com/sheepzh/time-tracker-for-browser')).toEqual(['github.com/sheepzh', 'github.com']) + expect(matchCond(cond, 'https://github.com/sheepzh/time-tracker-for-browser')).toEqual(['github.com', 'github.com/sheepzh']) expect(matchCond(cond, 'https://www.github.com')).toEqual([]) + expect(matchCond(cond, 'https://www.bilibili.com/cheese/list')).toEqual([]) + expect(matchCond(cond, 'https://www.bilibili.com/vedio')).toEqual(['*.bilibili.com*']) }) test('meetLimit', () => { From 5d1d45a22449e36ad3e8968993460a5ed6a05e51 Mon Sep 17 00:00:00 2001 From: sheepie Date: Tue, 30 Sep 2025 14:05:48 +0800 Subject: [PATCH 061/298] refactor: optimize bundle size (#555) --- .github/workflows/publish-all.yml | 27 +++++++++++ package.json | 2 - rspack/rspack.analyze.ts | 12 ++--- rspack/rspack.common.ts | 18 +++----- rspack/rspack.prod.ts | 5 +-- rspack/util.ts | 7 +++ script/psl.ts | 9 ++-- src/api/chrome/script.ts | 1 + src/api/crowdin.ts | 14 +++--- src/content-script/limit/modal/Main.tsx | 2 +- .../limit/modal/components/Alert.tsx | 2 +- .../limit/modal/components/Reason.tsx | 2 +- src/content-script/limit/modal/context.ts | 8 ++-- src/i18n/element.ts | 24 +++++----- src/pages/app/Layout/index.tsx | 2 +- .../app/components/About/Description.tsx | 3 +- .../AnalysisFilter/TargetSelect.tsx | 5 +-- .../components/Trend/Dimension/index.tsx | 4 +- .../Analysis/components/Trend/index.tsx | 6 +-- .../components/Timeline/Chart/index.tsx | 2 +- .../components/Timeline/Chart/useMerge.ts | 2 +- .../Dashboard/components/Timeline/Summary.tsx | 2 +- src/pages/app/components/Dashboard/index.tsx | 3 +- .../Habit/components/Period/Summary.tsx | 2 +- .../Habit/components/Period/index.tsx | 2 +- .../Habit/components/Site/Summary.tsx | 2 +- .../Habit/components/Site/index.tsx | 2 +- .../Limit/LimitModify/Sop/Step2.tsx | 2 +- .../Limit/LimitModify/Sop/Step3/TimeInput.tsx | 5 +-- .../Limit/LimitModify/Sop/context.ts | 2 +- .../app/components/Limit/LimitTable/index.tsx | 16 ++++--- src/pages/app/components/Limit/context.ts | 3 +- src/pages/app/components/Option/index.tsx | 2 +- .../components/Report/ReportList/index.tsx | 4 +- .../components/Report/ReportTable/index.tsx | 3 +- src/pages/app/components/Report/index.tsx | 2 +- .../category/CategorySelect/SelectFooter.tsx | 3 +- src/pages/app/index.ts | 5 +-- src/pages/hooks/index.ts | 45 +++---------------- src/pages/hooks/useCached.ts | 13 +++++- src/pages/hooks/useDebounce.ts | 43 ++++++++++++++++++ src/pages/hooks/useDocumentVisibility.ts | 15 +++++++ src/pages/hooks/useEcharts.ts | 6 +-- src/pages/hooks/useLocalStorage.ts | 30 ++++++++----- src/pages/hooks/useManualRequest.ts | 17 +++++++ src/pages/hooks/useMediaSize.ts | 2 +- src/pages/hooks/useRequest.ts | 18 +++++--- src/pages/hooks/useShadow.ts | 5 ++- src/pages/hooks/useState.ts | 20 ++++++--- src/pages/hooks/useWindowFocus.ts | 15 +++++++ src/pages/hooks/useWindowListener.ts | 12 +++++ src/pages/hooks/useWindowSize.ts | 20 +++++++++ .../popup/components/Header/LangSelect.tsx | 3 +- .../components/Percentage/Cate/index.tsx | 25 +++++------ .../components/Percentage/Site/index.tsx | 4 +- src/pages/popup/context.ts | 3 +- src/pages/popup/index.ts | 2 +- tsconfig.json | 5 +-- 58 files changed, 327 insertions(+), 193 deletions(-) create mode 100644 rspack/util.ts create mode 100644 src/pages/hooks/useDebounce.ts create mode 100644 src/pages/hooks/useDocumentVisibility.ts create mode 100644 src/pages/hooks/useManualRequest.ts create mode 100644 src/pages/hooks/useWindowFocus.ts create mode 100644 src/pages/hooks/useWindowListener.ts create mode 100644 src/pages/hooks/useWindowSize.ts diff --git a/.github/workflows/publish-all.yml b/.github/workflows/publish-all.yml index 6b2bfb642..7f08ba52c 100644 --- a/.github/workflows/publish-all.yml +++ b/.github/workflows/publish-all.yml @@ -2,6 +2,9 @@ name: Publish Extension to All Stores on: [workflow_dispatch] +env: + MAX_PACKAGE_SIZE: 2621440 # 2.5MB in bytes + jobs: build: runs-on: ubuntu-latest @@ -25,6 +28,30 @@ jobs: - name: Build for Firefox run: npm run build:firefox + - name: Check file sizes + run: | + # Check Chrome/Edge package size + CHROME_SIZE=$(ls -l market_packages/target.zip | awk '{print $5}') + echo "Chrome/Edge package: $CHROME_SIZE bytes" + + if [ $CHROME_SIZE -gt $MAX_PACKAGE_SIZE ]; then + echo "❌ Error: Chrome/Edge package exceeds size limit" + exit 1 + else + echo "✅ Chrome/Edge package size is within limit" + fi + + # Check Firefox package size + FIREFOX_SIZE=$(ls -l market_packages/target.firefox.zip | awk '{print $5}') + echo "Firefox package: $FIREFOX_SIZE bytes" + + if [ $FIREFOX_SIZE -gt $MAX_PACKAGE_SIZE ]; then + echo "❌ Error: Firefox package exceeds size limit" + exit 1 + else + echo "✅ Firefox package size is within limit" + fi + - name: Upload Chrome/Edge Artifact uses: actions/upload-artifact@v4 with: diff --git a/package.json b/package.json index 5470035a3..875dd766c 100644 --- a/package.json +++ b/package.json @@ -69,13 +69,11 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", - "@vueuse/core": "^13.9.0", "countup.js": "^2.9.0", "echarts": "^6.0.0", "element-plus": "2.11.4", "js-base64": "^3.7.8", "punycode": "^2.3.1", - "stream-browserify": "^3.0.0", "vue": "^3.5.22", "vue-router": "^4.5.1" }, diff --git a/rspack/rspack.analyze.ts b/rspack/rspack.analyze.ts index 17e3f1cee..873293b12 100644 --- a/rspack/rspack.analyze.ts +++ b/rspack/rspack.analyze.ts @@ -1,13 +1,7 @@ import { RsdoctorRspackPlugin } from "@rsdoctor/rspack-plugin" -import path from "path" -import manifest from "../src/manifest" -import optionGenerator from "./rspack.common" +import option from "./rspack.prod" +import { enhancePluginWith } from './util' -const outputPath = path.resolve(__dirname, '..', 'dist_prod') -const option = optionGenerator({ outputPath, manifest, mode: "production" }) - -const { plugins = [] } = option -plugins.push(new RsdoctorRspackPlugin()) -option.plugins = plugins +enhancePluginWith(option, new RsdoctorRspackPlugin()) export default option \ No newline at end of file diff --git a/rspack/rspack.common.ts b/rspack/rspack.common.ts index 204b72b48..36128a728 100644 --- a/rspack/rspack.common.ts +++ b/rspack/rspack.common.ts @@ -109,21 +109,15 @@ const staticOptions: Configuration = { resolve: { extensions: ['.ts', '.tsx', ".js", '.css', '.scss', '.sass'], tsConfig: join(__dirname, '..', 'tsconfig.json'), - fallback: { - // fallbacks of axios's dependencies start - stream: require.resolve('stream-browserify'), - zlib: false, - https: false, - http: false, - url: false, - assert: false, - // fallbacks of axios's dependencies end - } }, optimization: { splitChunks: { chunks: chunkFilter, cacheGroups: { + elementPlus: { + name: 'element-plus', + test: /[\\/]node_modules[\\/]element-plus[\\/]/, + }, defaultVendors: { filename: 'vendor/[name].js' } @@ -191,10 +185,8 @@ const generateOption = ({ outputPath, manifest, mode }: Option) => { filename: '[name].js', }, plugins, mode, - } - if (mode === "development") { // no eval with development, but generate *.map.js - config.devtool = 'cheap-module-source-map' + devtool: mode === 'development' ? 'cheap-module-source-map' : false, } return config } diff --git a/rspack/rspack.prod.ts b/rspack/rspack.prod.ts index 139f0a555..47df7f112 100644 --- a/rspack/rspack.prod.ts +++ b/rspack/rspack.prod.ts @@ -2,6 +2,7 @@ import path from "path" import manifest from "../src/manifest" import { FileManagerPlugin } from "./plugins/file-manager" import optionGenerator from "./rspack.common" +import { enhancePluginWith } from './util' const { name, version } = require(path.join(__dirname, '..', 'package.json')) @@ -33,9 +34,7 @@ const filemanagerPlugin = new FileManagerPlugin({ const option = optionGenerator({ outputPath, manifest, mode: "production" }) -const { plugins = [] } = option -plugins.push(filemanagerPlugin) -option.plugins = plugins +enhancePluginWith(option, filemanagerPlugin) option.devtool = false export default option \ No newline at end of file diff --git a/rspack/util.ts b/rspack/util.ts new file mode 100644 index 000000000..65114cee9 --- /dev/null +++ b/rspack/util.ts @@ -0,0 +1,7 @@ +import { type RspackOptions, type RspackPluginInstance } from '@rspack/core' + +export function enhancePluginWith(option: RspackOptions, ...toPush: RspackPluginInstance[]) { + const { plugins = [] } = option + plugins.push(...toPush) + option.plugins = plugins +} \ No newline at end of file diff --git a/script/psl.ts b/script/psl.ts index 374113f19..aea4ced6d 100644 --- a/script/psl.ts +++ b/script/psl.ts @@ -1,19 +1,18 @@ /** * Build psl tree */ - +import { fetchGet } from '@api/http' import { type PslTree } from '@util/psl' -import axios from 'axios' import { writeFileSync } from 'fs' import path from 'path' -import punycode from "punycode/" +import punycode from "punycode" const LIST_URL = "https://publicsuffix.org/list/effective_tld_names.dat" const JSON_PATH = path.join(__dirname, "..", "src", "util", "psl", "rules.json") const downloadList = async (): Promise => { - const response = await axios.get(LIST_URL) - return response.data + const response = await fetchGet(LIST_URL) + return response.text() } const parse = (tree: PslTree, parts: string[], index: number) => { diff --git a/src/api/chrome/script.ts b/src/api/chrome/script.ts index 88a880af6..e247df95a 100644 --- a/src/api/chrome/script.ts +++ b/src/api/chrome/script.ts @@ -6,6 +6,7 @@ export async function executeScript(tabId: number, files: string[]): Promise executeScriptMv2(tabId, file))) diff --git a/src/api/crowdin.ts b/src/api/crowdin.ts index 127774e88..c25f34261 100644 --- a/src/api/crowdin.ts +++ b/src/api/crowdin.ts @@ -6,7 +6,7 @@ */ import { CROWDIN_PROJECT_ID } from "@util/constant/url" -import axios, { type AxiosResponse } from "axios" +import { fetchGet } from './http' /** * Used to obtain translation status @@ -31,10 +31,8 @@ export async function getTranslationStatus(): Promise { const limit = 500 const auth = `Bearer ${PUBLIC_TOKEN}` const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/languages/progress?limit=${limit}` - const response: AxiosResponse = await axios.get(url, { - headers: { "Authorization": auth } - }) - const data: { data: { data: TranslationStatusInfo }[] } = response.data + const response = await fetchGet(url, { headers: { "Authorization": auth } }) + const data: { data: { data: TranslationStatusInfo }[] } = await response.json() return data.data.map(i => i.data) } @@ -46,10 +44,8 @@ export async function getMembers(): Promise { let offset = 0 while (true) { const url = `https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/members?limit=${limit}&offset=${offset}` - const response: AxiosResponse = await axios.get(url, { - headers: { "Authorization": auth } - }) - const data: { data: { data: MemberInfo }[] } = response.data + const response = await fetchGet(url, { headers: { "Authorization": auth } }) + const data: { data: { data: MemberInfo }[] } = await response.json() const newItems = data?.data?.map(i => i.data) ?? [] result.push(...newItems) diff --git a/src/content-script/limit/modal/Main.tsx b/src/content-script/limit/modal/Main.tsx index b80353944..f43e6ee74 100644 --- a/src/content-script/limit/modal/Main.tsx +++ b/src/content-script/limit/modal/Main.tsx @@ -3,7 +3,7 @@ import Alert from "./components/Alert" import Footer from "./components/Footer" import Reason from "./components/Reason" import { provideRule } from "./context" -import "./style" +import "./style/index.sass" const _default = defineComponent(() => { provideRule() diff --git a/src/content-script/limit/modal/components/Alert.tsx b/src/content-script/limit/modal/components/Alert.tsx index f57cf2fde..048e76b97 100644 --- a/src/content-script/limit/modal/components/Alert.tsx +++ b/src/content-script/limit/modal/components/Alert.tsx @@ -1,6 +1,6 @@ import { getUrl } from "@api/chrome/runtime" import { t } from "@cs/locale" -import { useRequest } from "@hooks" +import { useRequest } from "@hooks/useRequest" import optionHolder from "@service/components/option-holder" import { defineComponent } from "vue" diff --git a/src/content-script/limit/modal/components/Reason.tsx b/src/content-script/limit/modal/components/Reason.tsx index 3f905d562..7db01b007 100644 --- a/src/content-script/limit/modal/components/Reason.tsx +++ b/src/content-script/limit/modal/components/Reason.tsx @@ -3,7 +3,7 @@ import { useRequest } from "@hooks/useRequest" import Flex from "@pages/components/Flex" import { matchCond, meetLimit, meetTimeLimit, period2Str } from "@util/limit" import { formatPeriodCommon, MILL_PER_SECOND } from "@util/time" -import { ElDescriptions, ElDescriptionsItem, ElTag } from "element-plus" +import { ElDescriptions, ElDescriptionsItem, ElTag } from 'element-plus' import { computed, defineComponent } from "vue" import { useGlobalParam, useReason, useRule } from "../context" diff --git a/src/content-script/limit/modal/context.ts b/src/content-script/limit/modal/context.ts index e45b2ec01..1da91a102 100644 --- a/src/content-script/limit/modal/context.ts +++ b/src/content-script/limit/modal/context.ts @@ -1,7 +1,7 @@ -import { useRequest } from "@hooks/useRequest" +import { useRequest } from '@hooks/useRequest' +import { useWindowFocus } from '@hooks/useWindowFocus' import limitService from "@service/limit-service" -import { useWindowFocus } from "@vueuse/core" -import { type App, inject, provide, type Ref, ref, watch } from "vue" +import { type App, inject, provide, type Ref, shallowRef, watch } from "vue" import { type LimitReason } from "../common" const REASON_KEY = "display_reason" @@ -20,7 +20,7 @@ export const provideGlobalParam = (app: App, gp: GlobalParam) => { export const useGlobalParam = () => inject(GLOBAL_KEY) as GlobalParam export const provideReason = (app: App): Ref => { - const reason = ref() + const reason = shallowRef() app.provide(REASON_KEY, reason) return reason } diff --git a/src/i18n/element.ts b/src/i18n/element.ts index ed0791364..71c03d822 100644 --- a/src/i18n/element.ts +++ b/src/i18n/element.ts @@ -1,21 +1,21 @@ import ElementPlus from 'element-plus' -import { type Language } from "element-plus/lib/locale" +import { type Language } from "element-plus/es/locale" import { type App } from "vue" import { locale, t } from "." import calendarMessages from "./message/common/calendar" const LOCALES: { [locale in timer.Locale]: () => Promise<{ default: Language }> } = { - zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), - zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), - en: () => import('element-plus/lib/locale/lang/en'), - ja: () => import('element-plus/lib/locale/lang/ja'), - pt_PT: () => import('element-plus/lib/locale/lang/pt'), - uk: () => import('element-plus/lib/locale/lang/uk'), - es: () => import('element-plus/lib/locale/lang/es'), - de: () => import('element-plus/lib/locale/lang/de'), - fr: () => import('element-plus/lib/locale/lang/fr'), - ru: () => import('element-plus/lib/locale/lang/ru'), - ar: () => import('element-plus/lib/locale/lang/ar'), + zh_CN: () => import('element-plus/es/locale/lang/zh-cn'), + zh_TW: () => import('element-plus/es/locale/lang/zh-tw'), + en: () => import('element-plus/es/locale/lang/en'), + ja: () => import('element-plus/es/locale/lang/ja'), + pt_PT: () => import('element-plus/es/locale/lang/pt'), + uk: () => import('element-plus/es/locale/lang/uk'), + es: () => import('element-plus/es/locale/lang/es'), + de: () => import('element-plus/es/locale/lang/de'), + fr: () => import('element-plus/es/locale/lang/fr'), + ru: () => import('element-plus/es/locale/lang/ru'), + ar: () => import('element-plus/es/locale/lang/ar'), } export const initElementLocale = async (app: App) => { diff --git a/src/pages/app/Layout/index.tsx b/src/pages/app/Layout/index.tsx index 275a8ebc9..d55db02a0 100644 --- a/src/pages/app/Layout/index.tsx +++ b/src/pages/app/Layout/index.tsx @@ -12,7 +12,7 @@ import { defineComponent, type StyleValue } from "vue" import { RouterView } from "vue-router" import HeadNav from "./menu/Nav" import SideMenu from "./menu/Side" -import "./style" +import "./style.sass" import VersionTag from "./VersionTag" const _default = defineComponent(() => { diff --git a/src/pages/app/components/About/Description.tsx b/src/pages/app/components/About/Description.tsx index 21234ac1e..844e14f6f 100644 --- a/src/pages/app/components/About/Description.tsx +++ b/src/pages/app/components/About/Description.tsx @@ -1,6 +1,5 @@ import { t } from "@app/locale" -import { useMediaSize } from "@hooks" -import { MediaSize } from "@hooks/useMediaSize" +import { MediaSize, useMediaSize } from "@hooks" import { locale } from "@i18n" import Flex from "@pages/components/Flex" import metaService from "@service/meta-service" diff --git a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx index 2b41871bb..f6cad9265 100644 --- a/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx +++ b/src/pages/app/components/Analysis/components/AnalysisFilter/TargetSelect.tsx @@ -1,11 +1,10 @@ import { useCategories } from "@app/context" import { t } from "@app/locale" -import { useRequest, useState } from "@hooks" +import { useDebounce, useRequest, useState } from "@hooks" import Flex from "@pages/components/Flex" import siteService from "@service/site-service" import statService from "@service/stat-service" import { identifySiteKey, parseSiteKeyFromIdentity, SiteMap } from "@util/site" -import { useDebounce } from "@vueuse/core" import { ElSelectV2, ElTag, useNamespace } from "element-plus" import type { OptionType } from "element-plus/es/components/select-v2/src/select.types" import { computed, defineComponent, type FunctionalComponent, onMounted, ref, type StyleValue } from "vue" @@ -118,7 +117,7 @@ const TargetSelect = defineComponent(() => { ) const [query, setQuery] = useState('') - const debouncedQuery = useDebounce(query, 50) + const debouncedQuery = useDebounce(query, 50) const options = computed(() => { const q = debouncedQuery.value?.trim?.() diff --git a/src/pages/app/components/Analysis/components/Trend/Dimension/index.tsx b/src/pages/app/components/Analysis/components/Trend/Dimension/index.tsx index 704477be6..20af5eb1b 100644 --- a/src/pages/app/components/Analysis/components/Trend/Dimension/index.tsx +++ b/src/pages/app/components/Analysis/components/Trend/Dimension/index.tsx @@ -5,13 +5,13 @@ * https://opensource.org/licenses/MIT */ import { formatValue, type DimensionEntry, type RingValue, type ValueFormatter } from "@app/components/Analysis/util" +import { GRID_CELL_STYLE } from '@app/components/common/grid' import { KanbanIndicatorCell } from "@app/components/common/kanban" import { cvt2LocaleTime } from "@app/util/time" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Box from "@pages/components/Box" import Flex from "@pages/components/Flex" import { defineComponent } from "vue" -import { GRID_CELL_STYLE } from "../../../../common/grid" import Chart from "./Chart" export type DimensionData = { diff --git a/src/pages/app/components/Analysis/components/Trend/index.tsx b/src/pages/app/components/Analysis/components/Trend/index.tsx index d03620a2e..3f7850dfb 100644 --- a/src/pages/app/components/Analysis/components/Trend/index.tsx +++ b/src/pages/app/components/Analysis/components/Trend/index.tsx @@ -4,17 +4,17 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import { GRID_WRAPPER_STYLE } from '@app/components/common/grid' import { KanbanCard } from "@app/components/common/kanban" import { t } from "@app/locale" import { periodFormatter } from "@app/util/time" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { defineComponent } from "vue" -import { useAnalysisTimeFormat } from "../../context" +import { useAnalysisTimeFormat } from '../../context' import { initAnalysisTrend } from "./context" import Dimension from "./Dimension" import Filter from "./Filter" -import { GRID_WRAPPER_STYLE } from "../../../common/grid" import Total from "./Total" const visitFormatter = (val: number | undefined) => (Number.isInteger(val) ? val?.toString() : val?.toFixed(1)) ?? '-' diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx index 85cc5e3be..3008520dc 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/index.tsx @@ -9,7 +9,7 @@ import ChartTitle from '@app/components/Dashboard/ChartTitle' import { t } from '@app/locale' import { TIMELINE_LIFE_CYCLE } from '@db/timeline-database' import { Collection, Files, Link } from '@element-plus/icons-vue' -import { useShadow } from '@hooks/index' +import { useShadow } from '@hooks' import { useEcharts } from "@hooks/useEcharts" import Flex from "@pages/components/Flex" import { type ECElementEvent, type ECharts } from "echarts/core" diff --git a/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts b/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts index c5bdfe4e3..c7113b39e 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts +++ b/src/pages/app/components/Dashboard/components/Timeline/Chart/useMerge.ts @@ -3,7 +3,7 @@ import { t } from '@app/locale' import mergeRuleDatabase from '@db/merge-rule-database' import siteDatabase from '@db/site-database' import { TIMELINE_LIFE_CYCLE } from '@db/timeline-database' -import { useState } from '@hooks/index' +import { useState } from '@hooks' import CustomizedHostMergeRuler from '@service/components/host-merge-ruler' import { toMap } from '@util/array' import { CATE_NOT_SET_ID } from '@util/site' diff --git a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx index 5cff96f6a..0aec23a29 100644 --- a/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx +++ b/src/pages/app/components/Dashboard/components/Timeline/Summary.tsx @@ -1,6 +1,6 @@ import { t } from '@app/locale' import { InfoFilled } from '@element-plus/icons-vue' -import { useShadow } from '@hooks/index' +import { useShadow } from '@hooks' import Flex from '@pages/components/Flex' import { groupBy } from '@util/array' import { MILL_PER_HOUR, MILL_PER_MINUTE } from '@util/time' diff --git a/src/pages/app/components/Dashboard/index.tsx b/src/pages/app/components/Dashboard/index.tsx index ce484f67d..ccd777dc5 100644 --- a/src/pages/app/components/Dashboard/index.tsx +++ b/src/pages/app/components/Dashboard/index.tsx @@ -6,8 +6,7 @@ */ import { t } from "@app/locale" -import { useManualRequest, useMediaSize, useRequest } from "@hooks" -import { MediaSize, useXsState } from "@hooks/useMediaSize" +import { MediaSize, useManualRequest, useMediaSize, useRequest, useXsState } from "@hooks" import { isTranslatingLocale, locale } from "@i18n" import Flex from "@pages/components/Flex" import metaService from "@service/meta-service" diff --git a/src/pages/app/components/Habit/components/Period/Summary.tsx b/src/pages/app/components/Habit/components/Period/Summary.tsx index 1f00219a9..8acabafb7 100644 --- a/src/pages/app/components/Habit/components/Period/Summary.tsx +++ b/src/pages/app/components/Habit/components/Period/Summary.tsx @@ -2,7 +2,7 @@ import { GRID_CELL_STYLE } from "@app/components/common/grid" import { KanbanIndicatorCell } from "@app/components/common/kanban" import { t } from "@app/locale" import { periodFormatter } from "@app/util/time" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { averageByDay } from "@util/period" import { formatTime } from "@util/time" diff --git a/src/pages/app/components/Habit/components/Period/index.tsx b/src/pages/app/components/Habit/components/Period/index.tsx index 375be5f57..dc94f404b 100644 --- a/src/pages/app/components/Habit/components/Period/index.tsx +++ b/src/pages/app/components/Habit/components/Period/index.tsx @@ -8,7 +8,7 @@ import { GRID_CELL_STYLE } from "@app/components/common/grid" import { KanbanCard } from "@app/components/common/kanban" import { t } from "@app/locale" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { defineComponent } from "vue" import Average from "./Average" diff --git a/src/pages/app/components/Habit/components/Site/Summary.tsx b/src/pages/app/components/Habit/components/Site/Summary.tsx index 5b173793c..9b1d413e7 100644 --- a/src/pages/app/components/Habit/components/Site/Summary.tsx +++ b/src/pages/app/components/Habit/components/Site/Summary.tsx @@ -2,7 +2,7 @@ import { GRID_CELL_STYLE } from "@app/components/common/grid" import { KanbanIndicatorCell } from "@app/components/common/kanban" import { t } from "@app/locale" import { periodFormatter } from "@app/util/time" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { sum } from "@util/array" import { getHost } from "@util/stat" diff --git a/src/pages/app/components/Habit/components/Site/index.tsx b/src/pages/app/components/Habit/components/Site/index.tsx index 7df802dd6..6eed0217b 100644 --- a/src/pages/app/components/Habit/components/Site/index.tsx +++ b/src/pages/app/components/Habit/components/Site/index.tsx @@ -8,7 +8,7 @@ import { GRID_CELL_STYLE, GRID_WRAPPER_STYLE } from "@app/components/common/grid" import { KanbanCard } from "@app/components/common/kanban" import { t } from "@app/locale" -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import Flex from "@pages/components/Flex" import { computed, defineComponent, type StyleValue } from "vue" import { initProvider } from "./context" diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx index 47fb2e487..db8b40fb6 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx @@ -7,7 +7,7 @@ import { t } from "@app/locale" import { Delete, WarnTriangleFilled } from "@element-plus/icons-vue" -import { useState } from "@hooks/index" +import { useState } from "@hooks" import Flex from "@pages/components/Flex" import { cleanCond } from "@util/limit" import { ElButton, ElDivider, ElIcon, ElInput, ElLink, ElMessage, ElScrollbar, ElText, type ScrollbarInstance } from "element-plus" diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx index 5a70848ff..fde155be6 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx @@ -1,8 +1,7 @@ import { CircleClose, Clock } from "@element-plus/icons-vue" -import { useState } from "@hooks" +import { useDebounceFn, useState } from "@hooks" import { getStyle } from "@pages/util/style" import { range } from "@util/array" -import { useDebounceFn } from "@vueuse/core" import { Effect, ElIcon, ElInput, ElPopover, ElScrollbar, ScrollbarInstance, useLocale, useNamespace } from "element-plus" import { computed, defineComponent, nextTick, onMounted, ref, Transition, watch } from "vue" @@ -79,7 +78,7 @@ const TimeSpinner = defineComponent({ const estimatedIdx = Math.round((scrollTop - (scrollbarH * 0.5 - 10) / itemH + 3) / itemH) const value = Math.min(estimatedIdx, props.max - 1) debounceChangeValue(value) - }) + }, { passive: true }) } onMounted(() => { diff --git a/src/pages/app/components/Limit/LimitModify/Sop/context.ts b/src/pages/app/components/Limit/LimitModify/Sop/context.ts index 5391caf09..bca28b0ef 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/context.ts +++ b/src/pages/app/components/Limit/LimitModify/Sop/context.ts @@ -1,5 +1,5 @@ import { t } from "@app/locale" -import { useProvide, useProvider } from "@hooks/useProvider" +import { useProvide, useProvider } from "@hooks" import { range } from "@util/array" import { ElMessage } from "element-plus" import { type Reactive, reactive, ref, toRaw } from "vue" diff --git a/src/pages/app/components/Limit/LimitTable/index.tsx b/src/pages/app/components/Limit/LimitTable/index.tsx index 443f0d329..3e5615a09 100644 --- a/src/pages/app/components/Limit/LimitTable/index.tsx +++ b/src/pages/app/components/Limit/LimitTable/index.tsx @@ -6,13 +6,12 @@ */ import ColumnHeader from "@app/components/common/ColumnHeader" import { t } from "@app/locale" -import { useRequest } from "@hooks" +import { useLocalStorage, useRequest, useState } from "@hooks" import { type ElTableRowScope } from "@pages/element-ui/table" import weekHelper from "@service/components/week-helper" import { isEffective } from "@util/limit" -import { useLocalStorage } from "@vueuse/core" import { ElSwitch, ElTable, ElTableColumn, ElTag, type Sort } from "element-plus" -import { defineComponent } from "vue" +import { defineComponent, watch } from "vue" import { useLimitTable } from "../context" import LimitOperationColumn from "./column/LimitOperationColumn" import RuleContent from "./RuleContent" @@ -41,7 +40,12 @@ const _default = defineComponent(() => { changeEnabled, changeDelay, changeLocked } = useLimitTable() - const historySort = useLocalStorage('__limit_sort_default__', { prop: DEFAULT_SORT_COL, order: 'descending' }) + const [cachedSort, setCachedSort] = useLocalStorage( + '__limit_sort_default__', { prop: DEFAULT_SORT_COL, order: 'descending' } + ) + + const [sort, setSort] = useState(cachedSort) + watch(sort, () => setCachedSort(sort.value)) return () => ( { style={{ width: "100%" }} height="100%" data={list.value} - defaultSort={historySort.value} - onSort-change={(val: Sort) => historySort.value = { prop: val?.prop, order: val?.order }} + defaultSort={sort.value} + onSort-change={(val: Sort) => setSort({ prop: val?.prop, order: val?.order })} > { const filterOption = useReportFilter() diff --git a/src/pages/app/components/Report/ReportTable/index.tsx b/src/pages/app/components/Report/ReportTable/index.tsx index d73eae2e9..67427f7ff 100644 --- a/src/pages/app/components/Report/ReportTable/index.tsx +++ b/src/pages/app/components/Report/ReportTable/index.tsx @@ -10,7 +10,7 @@ import Pagination from "@app/components/common/Pagination" import { t } from "@app/locale" import { periodFormatter } from "@app/util/time" import { Histogram } from "@element-plus/icons-vue" -import { useManualRequest, useRequest, useState } from "@hooks" +import { useDocumentVisibility, useManualRequest, useRequest, useState } from "@hooks" import Flex from "@pages/components/Flex" import siteService from "@service/site-service" import statService, { type SiteQuery } from "@service/stat-service" @@ -18,7 +18,6 @@ import { sum } from "@util/array" import { isRtl } from "@util/document" import { siteEqual } from "@util/site" import { getAlias, isSite } from "@util/stat" -import { useDocumentVisibility } from "@vueuse/core" import { ElLink, ElTable, ElTableColumn, ElText, ElTooltip, type TableInstance } from "element-plus" import { computed, defineComponent, ref, watch } from "vue" import { queryPage } from "../common" diff --git a/src/pages/app/components/Report/index.tsx b/src/pages/app/components/Report/index.tsx index 08f924597..c0c56499f 100644 --- a/src/pages/app/components/Report/index.tsx +++ b/src/pages/app/components/Report/index.tsx @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { useXsState } from "@hooks/useMediaSize" +import { useXsState } from "@hooks" import { defineComponent } from "vue" import ContentContainer from "../common/ContentContainer" import { initReportContext } from "./context" diff --git a/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx b/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx index 5f1df1f81..c2a7bc26e 100644 --- a/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx +++ b/src/pages/app/components/common/category/CategorySelect/SelectFooter.tsx @@ -1,8 +1,7 @@ import { useCategories } from "@app/context" import { t } from "@app/locale" import { Check, Close, Plus } from "@element-plus/icons-vue" -import { useState, useSwitch } from "@hooks" -import { useManualRequest } from "@hooks/useRequest" +import { useManualRequest, useState, useSwitch } from "@hooks" import Flex from "@pages/components/Flex" import cateService from "@service/cate-service" import { stopPropagationAfter } from "@util/document" diff --git a/src/pages/app/index.ts b/src/pages/app/index.ts index 0ee6cc398..5aaf1be17 100644 --- a/src/pages/app/index.ts +++ b/src/pages/app/index.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { listenMediaSizeChange } from "@hooks/useMediaSize" +import { listenMediaSizeChange } from "@hooks" import { initLocale } from "@i18n" import { initElementLocale } from "@i18n/element" import optionService from "@service/option-service" @@ -16,8 +16,7 @@ import '../../common/timer' import { initEcharts } from "./echarts" import Main from "./Layout" import installRouter from "./router" -// global css -import './styles' +import './styles/index.sass' async function main() { // Init theme with cache first diff --git a/src/pages/hooks/index.ts b/src/pages/hooks/index.ts index 6bd9a466d..ccc3c2aa0 100644 --- a/src/pages/hooks/index.ts +++ b/src/pages/hooks/index.ts @@ -1,48 +1,15 @@ -import type { Ref, WatchSource } from "vue" -import type { RequestOption, RequestResult } from "./useRequest" - -export declare function useState(defaultValue: T): [state: Ref, setter: (val: T) => void, reset: () => void] -export declare function useState(defaultValue?: T): [state: Ref, setter: (val?: T) => void, reset: () => void] - -export declare function useShadow(source: WatchSource): [Ref, setter: (val: T) => void, refresh: () => void] -export declare function useShadow(source: WatchSource, defaultValue: T): [Ref, setter: (val: T) => void, refresh: () => void] -export declare function useShadow(source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] - -export declare function useManualRequest

    ( - getter: (...p: P) => Awaitable, - option: MakeRequired, 'manual'>, 'defaultValue'>, -): RequestResult -export declare function useManualRequest

    ( - getter: (...p: P) => Awaitable, - option?: Omit, 'manual'>, -): RequestResult - -export declare function useRequest

    ( - getter: (...p: P) => Awaitable, - option: MakeRequired, 'defaultValue'>, -): RequestResult -export declare function useRequest

    ( - getter: (...p: P) => Awaitable, - option?: RequestOption, -): RequestResult - - -export declare function useCached(key: string, defaultValue: T, defaultFirst?: boolean): { data: Ref, setter: (val: T) => void } -export declare function useCached( - key: string | undefined, - defaultValue?: T, - defaultFirst?: boolean, -): { data: Ref, setter: (val: T | undefined) => void } - -export declare function useLocalStorage(key: string, defaultValue: T): [T, (val: T | undefined) => void] -export declare function useLocalStorage(key: string): [T, (val: T | undefined) => void] - export * from "./useCached" +export * from "./useDebounce" +export * from "./useDocumentVisibility" export * from "./useLocalStorage" +export * from "./useManualRequest" export * from "./useMediaSize" export * from "./useProvider" export * from "./useRequest" +export * from "./useScrollRequest" export * from "./useShadow" export * from "./useState" export * from "./useSwitch" +export * from "./useWindowFocus" +export * from "./useWindowSize" diff --git a/src/pages/hooks/useCached.ts b/src/pages/hooks/useCached.ts index e4fd56c0b..cd75bcc2e 100644 --- a/src/pages/hooks/useCached.ts +++ b/src/pages/hooks/useCached.ts @@ -33,7 +33,18 @@ const saveCache = (key: string, val: T) => { } } -export const useCached = (key: string | undefined, defaultValue?: T, defaultFirst?: boolean): Result => { +export function useCached(key: string, defaultValue: T, defaultFirst?: boolean): { data: Ref, setter: (val: T) => void } +export function useCached( + key: string | undefined, + defaultValue?: T, + defaultFirst?: boolean, +): { data: Ref, setter: (val: T | undefined) => void } + +export function useCached( + key: string | undefined, + defaultValue?: T, + defaultFirst?: boolean, +): Result { if (!key) { const [data, setter] = useState(defaultValue) return { data, setter } diff --git a/src/pages/hooks/useDebounce.ts b/src/pages/hooks/useDebounce.ts new file mode 100644 index 000000000..aa1e83031 --- /dev/null +++ b/src/pages/hooks/useDebounce.ts @@ -0,0 +1,43 @@ +import { Ref, shallowRef, type MaybeRefOrGetter } from 'vue' + +type FunctionArgs = (...args: any[]) => any + +const DEFAULT_TIMEOUT = 200 + +export function useDebounceFn( + fn: T, + ms?: MaybeRefOrGetter +): T { + let timeoutId: ReturnType | undefined + + const resolveValue = (value: MaybeRefOrGetter): number => { + if (typeof value === 'function') { + return value() + } else if (value && typeof value === 'object' && 'value' in value) { + return value.value + } + return value + } + + const debounced = ((...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId) + } + + const delay = ms !== undefined ? resolveValue(ms) : DEFAULT_TIMEOUT + + timeoutId = setTimeout(() => { + fn(...args) + }, delay) + }) as T + + return debounced +} + +export function useDebounce(original: Ref, ms?: MaybeRefOrGetter): Ref { + const inner = shallowRef(original.value) + + useDebounceFn(() => inner.value = original.value, ms) + + return inner +} \ No newline at end of file diff --git a/src/pages/hooks/useDocumentVisibility.ts b/src/pages/hooks/useDocumentVisibility.ts new file mode 100644 index 000000000..b30cb0568 --- /dev/null +++ b/src/pages/hooks/useDocumentVisibility.ts @@ -0,0 +1,15 @@ +import { onMounted, shallowRef } from 'vue' + +export function useDocumentVisibility() { + if (typeof document === 'undefined') return shallowRef('visible') + + const visibility = shallowRef(document.visibilityState) + + onMounted(() => { + const listener = () => visibility.value = document.visibilityState + document.addEventListener('visibilitychange', listener, { passive: true }) + return () => document.removeEventListener('visibilitychange', listener) + }) + + return visibility +} \ No newline at end of file diff --git a/src/pages/hooks/useEcharts.ts b/src/pages/hooks/useEcharts.ts index c979700a3..6afc2ddf3 100644 --- a/src/pages/hooks/useEcharts.ts +++ b/src/pages/hooks/useEcharts.ts @@ -5,9 +5,9 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import { useWindowSize } from "@hooks/useWindowSize" import optionHolder from "@service/components/option-holder" import { processAnimation, processAria, processRtl } from "@util/echarts" -import { useWindowSize } from "@vueuse/core" import { type AriaComponentOption, type ComposeOption } from "echarts" import { type ECharts, init } from "echarts/core" import { ElLoading } from "element-plus" @@ -85,7 +85,6 @@ export const useEcharts = void }): WrapperResult => { const elRef = ref() @@ -93,7 +92,6 @@ export const useEcharts = type StorageObject = { [key: string]: StorageValue } @@ -6,28 +7,35 @@ type StorageValue = | StorageArray | StorageObject -export const useLocalStorage = (key: string, defaultVal?: T): [data: T | undefined, setter: (val: T | undefined) => void] => { +export function useLocalStorage(key: string, defaultValue: T): [T, (val: T | undefined) => void] +export function useLocalStorage(key: string): [T | undefined, (val: T | undefined) => void] + +export function useLocalStorage(key: string, defaultVal?: T): [data: T | undefined, setter: (val: T | undefined) => void] { + const value = deserialize(localStorage.getItem(key), defaultVal) ?? defaultVal + const setter = (val: T | undefined) => { - if (val === undefined || val === null) { + if (val === undefined) { localStorage?.removeItem(key) } else { localStorage?.setItem(key, JSON.stringify(val)) } } - let storedVal = localStorage.getItem(key) - if (!storedVal) { - return [defaultVal, setter] - } + + return [value, setter] +} + +function deserialize(json: string | null, defaultVal?: T): T | undefined { + if (!json) return undefined try { - const stored = JSON.parse(storedVal) || {} + const stored = JSON.parse(json) || {} Object.entries(defaultVal || {}).forEach(([k, v]) => { if (stored[k] === undefined || stored[k] === null) { stored[k] = v } }) - return [stored, setter] - } catch { } - - return [defaultVal, setter] + return stored + } catch { + return undefined + } } \ No newline at end of file diff --git a/src/pages/hooks/useManualRequest.ts b/src/pages/hooks/useManualRequest.ts new file mode 100644 index 000000000..45dc09606 --- /dev/null +++ b/src/pages/hooks/useManualRequest.ts @@ -0,0 +1,17 @@ +import { type RequestOption, type RequestResult, useRequest } from './useRequest' + +export function useManualRequest

    ( + getter: (...p: P) => Awaitable, + option: MakeRequired, 'manual'>, 'defaultValue'>, +): RequestResult +export function useManualRequest

    ( + getter: (...p: P) => Awaitable, + option?: Omit, 'manual'>, +): RequestResult + +export function useManualRequest

    ( + getter: (...p: P) => Promise | T, + option?: Omit, 'manual'>, +): RequestResult { + return useRequest(getter, { ...option || {}, manual: true }) +} \ No newline at end of file diff --git a/src/pages/hooks/useMediaSize.ts b/src/pages/hooks/useMediaSize.ts index f3df56169..d659fa971 100644 --- a/src/pages/hooks/useMediaSize.ts +++ b/src/pages/hooks/useMediaSize.ts @@ -1,4 +1,4 @@ -import { useWindowSize } from "@vueuse/core" +import { useWindowSize } from "@hooks" import { computed } from "vue" export enum MediaSize { diff --git a/src/pages/hooks/useRequest.ts b/src/pages/hooks/useRequest.ts index 4cc60227e..3f6306dab 100644 --- a/src/pages/hooks/useRequest.ts +++ b/src/pages/hooks/useRequest.ts @@ -37,7 +37,19 @@ const findLoadingEl = async (target: RequestOption['loadingT return undefined } -export const useRequest =

    (getter: (...p: P) => Promise | T, option?: RequestOption): RequestResult => { +export function useRequest

    ( + getter: (...p: P) => Awaitable, + option: MakeRequired, 'defaultValue'>, +): RequestResult +export function useRequest

    ( + getter: (...p: P) => Awaitable, + option?: RequestOption, +): RequestResult + +export function useRequest

    ( + getter: (...p: P) => Promise | T, + option?: RequestOption, +): RequestResult { const { manual = false, defaultValue, defaultParam = ([] as any[] as P), @@ -81,8 +93,4 @@ export const useRequest =

    (getter: (...p: P) => Promise | } const refreshAgain = () => param.value && refresh(...param.value) return { data, ts, refresh, refreshAsync, refreshAgain, loading, param } -} - -export const useManualRequest =

    (getter: (...p: P) => Promise | T, option?: Omit, 'manual'>): RequestResult => { - return useRequest(getter, { ...option || {}, manual: true }) } \ No newline at end of file diff --git a/src/pages/hooks/useShadow.ts b/src/pages/hooks/useShadow.ts index c5467ae5f..cea89da56 100644 --- a/src/pages/hooks/useShadow.ts +++ b/src/pages/hooks/useShadow.ts @@ -1,6 +1,9 @@ import { type Ref, type WatchSource, ref, watch } from "vue" -export const useShadow = (source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] => { +export function useShadow(source: WatchSource): [Ref, setter: (val: T) => void, refresh: () => void] +export function useShadow(source: WatchSource, defaultValue: T): [Ref, setter: (val: T) => void, refresh: () => void] +export function useShadow(source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] +export function useShadow(source: WatchSource, defaultValue?: T): [Ref, setter: (val?: T) => void, refresh: () => void] { const getVal = () => typeof source === "function" ? source() : source?.value const initial = getVal() ?? defaultValue const shadow = initial ? ref(initial) : ref() diff --git a/src/pages/hooks/useState.ts b/src/pages/hooks/useState.ts index ece80575a..6ad8751c8 100644 --- a/src/pages/hooks/useState.ts +++ b/src/pages/hooks/useState.ts @@ -1,10 +1,20 @@ -import { ref, type Ref, shallowRef } from "vue" +import { type ShallowRef, shallowRef } from "vue" -export const useState = (defaultValue?: T): - | [state: Ref, setter: (val: T) => void, reset: () => void] - | [state: Ref, setter: (val?: T) => void, reset: () => void] => { +export function useState(defaultValue: T): [ + state: ShallowRef, + setter: (val: T) => void, + reset: () => void, +] +export function useState(defaultValue?: T): [ + state: ShallowRef, + setter: (val?: T) => void, + reset: () => void, +] +export function useState(defaultValue?: T): + | [state: ShallowRef, setter: (val: T) => void, reset: () => void] + | [state: ShallowRef, setter: (val?: T) => void, reset: () => void] { if (defaultValue === undefined || defaultValue === null) { - const result = ref() + const result = shallowRef() return [ result, (val?: T) => result.value = val, diff --git a/src/pages/hooks/useWindowFocus.ts b/src/pages/hooks/useWindowFocus.ts new file mode 100644 index 000000000..bbc77d95a --- /dev/null +++ b/src/pages/hooks/useWindowFocus.ts @@ -0,0 +1,15 @@ +import { shallowRef, type ShallowRef } from 'vue' +import { useWindowListener } from './useWindowListener' + +export function useWindowFocus(): ShallowRef { + if (typeof window === 'undefined') return shallowRef(false) + + const focused = shallowRef(window.document.hasFocus()) + + const options: AddEventListenerOptions = { passive: true } + + useWindowListener('focus', () => focused.value = true, options) + useWindowListener('blur', () => focused.value = false, options) + + return focused +} \ No newline at end of file diff --git a/src/pages/hooks/useWindowListener.ts b/src/pages/hooks/useWindowListener.ts new file mode 100644 index 000000000..dd3f97ae0 --- /dev/null +++ b/src/pages/hooks/useWindowListener.ts @@ -0,0 +1,12 @@ +import { onMounted } from 'vue' + +export const useWindowListener = (type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => { + if (typeof window === 'undefined') { + return + } + + onMounted(() => { + window.addEventListener(type, listener, options) + return () => window.removeEventListener(type, listener) + }) +} \ No newline at end of file diff --git a/src/pages/hooks/useWindowSize.ts b/src/pages/hooks/useWindowSize.ts new file mode 100644 index 000000000..ddb4e924e --- /dev/null +++ b/src/pages/hooks/useWindowSize.ts @@ -0,0 +1,20 @@ +import { onMounted, shallowRef } from 'vue' +import { useWindowListener } from './useWindowListener' + +export function useWindowSize() { + const width = shallowRef(Number.POSITIVE_INFINITY) + const height = shallowRef(Number.POSITIVE_INFINITY) + + const update = () => { + if (typeof window === 'undefined') return + width.value = window.innerWidth + height.value = window.innerHeight + } + + update() + onMounted(update) + + useWindowListener('resize', update, { passive: true }) + + return { width, height } +} diff --git a/src/pages/popup/components/Header/LangSelect.tsx b/src/pages/popup/components/Header/LangSelect.tsx index a5ec4ee5f..146833adf 100644 --- a/src/pages/popup/components/Header/LangSelect.tsx +++ b/src/pages/popup/components/Header/LangSelect.tsx @@ -1,5 +1,6 @@ import { createTab } from "@api/chrome/tab" -import { useManualRequest, useRequest } from "@hooks/useRequest" +import { useManualRequest } from "@hooks/useManualRequest" +import { useRequest } from "@hooks/useRequest" import { ALL_LOCALES, handleLocaleOption, localeSameAsBrowser, t } from "@i18n" import optionMessages from "@i18n/message/app/option" import localeMessages from "@i18n/message/common/locale" diff --git a/src/pages/popup/components/Percentage/Cate/index.tsx b/src/pages/popup/components/Percentage/Cate/index.tsx index 9b53cdac3..0f3d59d56 100644 --- a/src/pages/popup/components/Percentage/Cate/index.tsx +++ b/src/pages/popup/components/Percentage/Cate/index.tsx @@ -1,21 +1,20 @@ import { useEcharts } from "@hooks/useEcharts" import { usePopupContext } from "@popup/context" -import { defineComponent, type PropType, toRef, watch } from "vue" +import { defineComponent, toRef, watch } from "vue" import { type PercentageResult } from "../query" import Wrapper from "./Wrapper" -const Cate = defineComponent({ - props: { - value: Object as PropType, - }, - setup(props) { - const { darkMode } = usePopupContext() - const data = toRef(props, 'value') - const { elRef, refresh } = useEcharts(Wrapper, data, { watch: true }) - watch(darkMode, refresh) +type Props = { + value: PercentageResult | undefined +} - return () =>

    - } -}) +const Cate = defineComponent(props => { + const { darkMode } = usePopupContext() + const data = toRef(props, 'value') + const { elRef, refresh } = useEcharts(Wrapper, data) + watch(darkMode, refresh) + + return () =>
    +}, { props: ['value'] }) export default Cate \ No newline at end of file diff --git a/src/pages/popup/components/Percentage/Site/index.tsx b/src/pages/popup/components/Percentage/Site/index.tsx index cea505694..7d8788416 100644 --- a/src/pages/popup/components/Percentage/Site/index.tsx +++ b/src/pages/popup/components/Percentage/Site/index.tsx @@ -5,13 +5,13 @@ import { type PercentageResult } from "../query" import Wrapper from "./Wrapper" type Props = { - value: PercentageResult + value: PercentageResult | undefined } const Site = defineComponent(props => { const { darkMode } = usePopupContext() const data = toRef(props, 'value') - const { elRef, refresh } = useEcharts(Wrapper, data, { watch: true }) + const { elRef, refresh } = useEcharts(Wrapper, data) watch(darkMode, refresh) return () =>
    diff --git a/src/pages/popup/context.ts b/src/pages/popup/context.ts index 3ed3d6e07..e229cec65 100644 --- a/src/pages/popup/context.ts +++ b/src/pages/popup/context.ts @@ -1,5 +1,4 @@ -import { useLocalStorage, useRequest } from "@hooks" -import { useProvide, useProvider } from "@hooks/useProvider" +import { useLocalStorage, useProvide, useProvider, useRequest } from "@hooks" import cateService from "@service/cate-service" import optionService from "@service/option-service" import { toMap } from "@util/array" diff --git a/src/pages/popup/index.ts b/src/pages/popup/index.ts index a532b796e..2c62c3af5 100644 --- a/src/pages/popup/index.ts +++ b/src/pages/popup/index.ts @@ -14,7 +14,7 @@ import { createApp } from "vue" import Main from "./Main" import { type FrameRequest, type FrameResponse } from "./message" import initRouter from "./router" -import "./style" +import "./style/index.sass" function send2ParentWindow(data: any): Promise { return new Promise(resolve => { diff --git a/tsconfig.json b/tsconfig.json index 83217be2f..a0e34a5f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "baseUrl": "./", - "module": "CommonJS", - "target": "ESNext", + "module": "commonjs", + "target": "esnext", "lib": [ "ES2023" ], @@ -15,7 +15,6 @@ "strict": true, "resolveJsonModule": true, "importHelpers": true, - "moduleResolution": "node", "skipLibCheck": true, "paths": { "@api/*": [ From 2631c40b148e138b3c0f9ac7af6ab08159e84143 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 30 Sep 2025 15:23:08 +0800 Subject: [PATCH 062/298] fix: x-axis ticks conflict with labels (#556) --- script/user-chart/render.ts | 2 +- src/api/gist.ts | 4 ++-- src/api/http.ts | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/script/user-chart/render.ts b/script/user-chart/render.ts index f12a09516..da1b3d14c 100644 --- a/script/user-chart/render.ts +++ b/script/user-chart/render.ts @@ -136,7 +136,7 @@ function render2Svg(chartData: ChartData): string { grid: { left: '3%', right: '4%', - bottom: '3%', + bottom: '8%', containLabel: true }, xAxis: [{ diff --git a/src/api/gist.ts b/src/api/gist.ts index 53ee29746..86edd8bea 100644 --- a/src/api/gist.ts +++ b/src/api/gist.ts @@ -6,7 +6,7 @@ */ import FIFOCache from "@util/fifo-cache" -import { fetchGet, fetchPost } from "./http" +import { fetchGet, fetchGetWithTry, fetchPost } from "./http" type BaseFile = { filename: string @@ -144,7 +144,7 @@ export async function getJsonFileContent(file: File): Promise { if (!rawUrl) { return null } - const response = await fetchGet(rawUrl) + const response = await fetchGetWithTry(rawUrl, 3) return await response.json() } diff --git a/src/api/http.ts b/src/api/http.ts index a675bbc8a..6675ef081 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -5,6 +5,19 @@ export type FetchResult = { statusCode: number } +export async function fetchGetWithTry(url: string, maxTry: number, option?: Option): Promise { + let count = 0 + do { + count++ + try { + return await fetch(url, { ...option, method: "GET" }) + } catch (e) { + console.error(`Failed to fetch get: url=${url}, tryCnt=${count}, err=${e}`) + } + } while (count < maxTry) + throw Error(`Unable to obtain within the maximum number of attempts: url=${url}, maxCnt=${maxTry}`) +} + export async function fetchGet(url: string, option?: Option): Promise { try { const response = await fetch(url, { From 36ba97bc4d3560f02b4ec8c2bebdb47edf79c5dc Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 30 Sep 2025 15:28:26 +0800 Subject: [PATCH 063/298] build(deps-dev): bump @types/chrome from 0.1.12 to 0.1.14 --- package.json | 2 +- src/api/chrome/action.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 875dd766c..a1c87d6b1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.12", + "@types/chrome": "0.1.14", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.5.2", diff --git a/src/api/chrome/action.ts b/src/api/chrome/action.ts index 8ae11e46d..17aa26a54 100644 --- a/src/api/chrome/action.ts +++ b/src/api/chrome/action.ts @@ -10,8 +10,8 @@ export function setBadgeText(text: string, tabId: number | undefined): Promise { - let realColor: string | chrome.action.ColorArray = color ?? ( +export function setBadgeBgColor(color: string | chrome.extensionTypes.ColorArray | undefined): Promise { + let realColor: string | chrome.extensionTypes.ColorArray = color ?? ( // Use null to clear bg color for Firefox IS_FIREFOX ? null as unknown as string : [0, 0, 0, 0] ) From 727840b8e525379bb2e85ff1741a0fad0f9f5a24 Mon Sep 17 00:00:00 2001 From: KHDK Date: Tue, 30 Sep 2025 19:38:23 +0800 Subject: [PATCH 064/298] feat: revise the tip on GUI to reflect the new feature (#558) --- src/i18n/message/app/limit-resource.json | 15 ++++++++------- .../components/Limit/LimitModify/Sop/Step2.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index dfb779507..11d3bfad5 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -1,7 +1,7 @@ { "zh_CN": { "filterDisabled": "过滤无效规则", - "wildcardTip": "您可以使用通配符来匹配子域名或子页面!", + "wildcardTip": "您可以使用通配符来匹配子域名或子页面,使用\"+\"作为前缀来排除子页面!", "item": { "name": "规则名称", "condition": "限制网址", @@ -56,7 +56,7 @@ }, "zh_TW": { "filterDisabled": "過濾無效規則", - "wildcardTip": "你可以使用萬用字元來匹配子網域或子頁面!", + "wildcardTip": "你可以使用萬用字元來匹配子網域或子頁面,使用\"+\"作為前置詞來排除子頁面!", "item": { "name": "規則名稱", "condition": "限制網址", @@ -109,7 +109,7 @@ }, "en": { "filterDisabled": "Only enabled", - "wildcardTip": "You can use wildcards to match subdomains or subpages!", + "wildcardTip": "You can use wildcards to match subdomains or subpages, and use \"+\" as a prefix to exclude subpages!", "item": { "name": "Rule name", "condition": "Restricted URL", @@ -162,6 +162,7 @@ }, "ja": { "filterDisabled": "有效", + "wildcardTip": "ワイルドカードを使用してサブドメインまたはサブページに一致させることができ、「+」をプレフィックスとしてサブページを除外することができます!", "item": { "name": "規則名", "condition": "制限 URL", @@ -211,7 +212,7 @@ }, "pt_PT": { "filterDisabled": "Apenas ativos", - "wildcardTip": "Pode usar wildcards para corresponder a subdomínios ou subpáginas!", + "wildcardTip": "Pode usar wildcards para corresponder a subdomínios ou subpáginas, e usar o \"+\" como prefixo para excluir subpáginas!", "item": { "name": "Nome da regra", "condition": "URL restrito", @@ -311,7 +312,7 @@ }, "es": { "filterDisabled": "Solo habilitados", - "wildcardTip": "¡Puedes usar comodines para coincidir con subdominios o subpáginas!", + "wildcardTip": "¡Puedes usar comodines para coincidir con subdominios o subpáginas, y usar el \"+\" como prefijo para excluir subpáginas!", "item": { "name": "Nombre de la regla", "condition": "URL restringida", @@ -416,7 +417,7 @@ }, "fr": { "filterDisabled": "Activé uniquement", - "wildcardTip": "Vous pouvez utiliser des wildcards pour correspondre à des sous-domaines ou sous-pages !", + "wildcardTip": "Vous pouvez utiliser des wildcards pour correspondre à des sous-domaines ou sous-pages, et utiliser le \"+\" comme préfixe pour exclure des sous-pages !", "item": { "name": "Nom de règle", "condition": "URL restreinte", @@ -513,7 +514,7 @@ }, "ar": { "filterDisabled": "تم التمكين فقط", - "wildcardTip": "يمكنك استخدام الرموز البديلة لتشمل النطاقات أو الصفحات الفرعية!", + "wildcardTip": "يمكنك استخدام الرموز البديلة لتطابق النطاقات الفرعية أو الصفحات الفرعية، ويمكنك استخدام العلامة \"+\" كبادئة لاستبعاد الصفحات الفرعية!", "item": { "name": "اسم القاعدة", "condition": "عنوان URL مقيد", diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx index db8b40fb6..ade64e09f 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx @@ -54,7 +54,7 @@ const _default = defineComponent(() => { ) }} - placeholder="www.demo.com, *.demo.com, demo.com/blog/*, demo.com/**" + placeholder="www.demo.com, *.demo.com, demo.com/blog/*, demo.com/**, +www.demo.com/blog/list" /> {t(msg => msg.limit.wildcardTip)} From a75876fb2d1b68e97334f35a11c3a5e16314ecbe Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 30 Sep 2025 19:53:24 +0800 Subject: [PATCH 065/298] v3.6.5 --- CHANGELOG.md | 9 +++++++++ package.json | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0551f417..feb8287d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.5] - 2025-10-01 + +- Supported excluding subpages when setting restriction rules, thanks to [KHDKR](https://github.com/KHDKR) +- Optimized bundle size + +## [3.6.4] - 2025-09-20 + +- Use gray color for other items on the popup page + ## [3.6.3] - 2025-09-11 - Optimized the block page diff --git a/package.json b/package.json index a1c87d6b1..dc86ef6d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.4", + "version": "3.6.5", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { @@ -40,7 +40,7 @@ "@types/chrome": "0.1.14", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.5.2", + "@types/node": "^24.6.0", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^1.5.0", "babel-loader": "^10.0.0", @@ -80,4 +80,4 @@ "engines": { "node": ">=20" } -} +} \ No newline at end of file From 52a49cf6552a2df39ed0e6ba1d6af7b40b1d5417 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:38:48 +0800 Subject: [PATCH 066/298] chore(psl): update PSL list by bot (#559) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/util/psl/rules.json | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/util/psl/rules.json b/src/util/psl/rules.json index 14e97c0c4..1b4489a5f 100644 --- a/src/util/psl/rules.json +++ b/src/util/psl/rules.json @@ -299,12 +299,14 @@ "flutterflow": 1, "framer": 1, "github": 1, + "hackclub": 1, "hasura": 1, "hosted": { "c": { "*": 1 } }, + "leapcell": 1, "loginline": 1, "lovable": 1, "luyani": 1, @@ -1139,6 +1141,11 @@ "azure": 1, "ba": { "c": { + "brendly": { + "c": { + "shop": 1 + } + }, "com": 1, "edu": 1, "gov": 1, @@ -1827,6 +1834,7 @@ "cc": { "c": { "cleverapps": 1, + "cloud-ip": 1, "cloudns": 1, "csx": 1, "fantasyleague": 1, @@ -2441,6 +2449,7 @@ "3utilities": 1, "4u": 1, "a2hosted": 1, + "abrdns": 1, "adobeaemcloud": { "c": { "dev": { @@ -2452,8 +2461,6 @@ "l": 1 }, "africa": 1, - "airkitapps": 1, - "airkitapps-au": 1, "aivencloud": 1, "aliases121": 1, "alibabacloudcs": 1, @@ -4116,6 +4123,8 @@ "demo": 1 } }, + "jote-dr-lt1": 1, + "jote-rd-lt1": 1, "joyent": { "c": { "cns": { @@ -4185,7 +4194,6 @@ "mydatto": 1, "mydbserver": 1, "mydobiss": 1, - "mydrobo": 1, "myiphost": 1, "mynascloud": 1, "myqnapcloud": 1, @@ -4315,6 +4323,7 @@ "reservd": 1, "reserve-online": 1, "rhcloud": 1, + "rice-labs": 1, "routingthecloud": 1, "ru": 1, "sa": 1, @@ -4756,6 +4765,11 @@ "virtual-user": 1, "virtualuser": 1, "webspaceconfig": 1, + "xenonconnect": { + "c": { + "*": 1 + } + }, "xn--gnstigbestellen-zvb": 1, "xn--gnstigliefern-wob": 1 }, @@ -4872,6 +4886,7 @@ "*": 1 } }, + "leapcell": 1, "localcert": { "c": { "user": { @@ -5230,7 +5245,6 @@ }, "eu": { "c": { - "airkitapps": 1, "barsy": 1, "cloudns": 1, "diskstation": 1, @@ -5751,6 +5765,7 @@ "hospital": 1, "host": { "c": { + "bolt": 1, "cloudaccess": 1, "easypanel": 1, "fastvps": 1, @@ -5865,6 +5880,7 @@ "biz": 1, "co": 1, "desa": 1, + "e": 1, "go": 1, "kop": 1, "mil": 1, @@ -11222,6 +11238,7 @@ "barsy": 1, "eero": 1, "eero-stage": 1, + "leapcell": 1, "websitebuilder": 1 }, "l": 1 @@ -12207,6 +12224,7 @@ "*": 1 } }, + "canva": 1, "code": { "c": { "*": 1 @@ -12233,7 +12251,6 @@ "onporter": 1, "ravendb": 1, "repl": 1, - "servers": 1, "stackit": 1, "val": { "c": { @@ -12438,7 +12455,8 @@ "eu": 1, "us": 1 } - } + }, + "teleport": 1 }, "l": 1 }, @@ -12510,6 +12528,7 @@ "sourcecraft": 1, "square": 1, "srht": 1, + "support": 1, "tst": { "c": { "*": 1 @@ -13505,8 +13524,7 @@ "ms": { "c": { "cc": 1, - "k12": 1, - "lib": 1 + "k12": 1 }, "l": 1 }, From 92a346b4f12b264d4d7a762a515b21f08ef70b45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:39:33 +0800 Subject: [PATCH 067/298] build(deps-dev): bump typescript from 5.9.2 to 5.9.3 (#561) Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3) --- updated-dependencies: - dependency-name: typescript dependency-version: 5.9.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc86ef6d4..cbc85ceb2 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "5.9.2", + "typescript": "5.9.3", "url-loader": "^4.1.1" }, "optionalDependencies": { From d5b713fabffa7543dfb9607bf11ab942b3a0c345 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:43:05 +0800 Subject: [PATCH 068/298] build(deps-dev): bump @types/chrome from 0.1.14 to 0.1.16 (#560) Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.14 to 0.1.16. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome) --- updated-dependencies: - dependency-name: "@types/chrome" dependency-version: 0.1.16 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbc85ceb2..99947f107 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.14", + "@types/chrome": "0.1.16", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.6.0", From 45f64aa6b5de600fb40cf6a0e5afc1d2dee32835 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:02:02 +0800 Subject: [PATCH 069/298] build(deps-dev): bump @types/chrome from 0.1.16 to 0.1.22 (#564) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99947f107..3d29a2eb8 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.16", + "@types/chrome": "0.1.22", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", "@types/node": "^24.6.0", From 4a360e0640219221dea09761b043caf9fd602525 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 19:38:54 +0800 Subject: [PATCH 070/298] i18n(download): download translations by bot (#567) --- src/i18n/message/app/about-resource.json | 4 ++-- src/i18n/message/app/dashboard-resource.json | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/i18n/message/app/about-resource.json b/src/i18n/message/app/about-resource.json index 61d5c8e6b..a26e85e1b 100644 --- a/src/i18n/message/app/about-resource.json +++ b/src/i18n/message/app/about-resource.json @@ -35,13 +35,13 @@ }, "zh_TW": { "label": { - "name": "擴充名稱", + "name": "名稱", "version": "版本", "website": "官網", "installation": "安裝", "thanks": "致謝", "privacy": "隱私權", - "license": "授權", + "license": "許可證", "support": "支援" }, "text": { diff --git a/src/i18n/message/app/dashboard-resource.json b/src/i18n/message/app/dashboard-resource.json index 13e7e3278..23c25849f 100644 --- a/src/i18n/message/app/dashboard-resource.json +++ b/src/i18n/message/app/dashboard-resource.json @@ -40,6 +40,13 @@ }, "monthOnMonth": { "title": "瀏覽時數月環比趨勢" + }, + "timeline": { + "title": "最近 {n} 天的时间线", + "busyScore": "繁忙度", + "busyScoreDesc": "与與總瀏覽時間和每小時的網站數量有關。計算公式请查看源代碼", + "focusScore": "专注度", + "focusScoreDesc": "與連續瀏覽同一網站的總時間有關。計算公式请查看源代碼" } }, "en": { From 6512f7755dd7244346b7137cca0cc8f96dee7a79 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Thu, 9 Oct 2025 00:29:54 +0800 Subject: [PATCH 071/298] fix: style error of the translation page (#568) --- package.json | 8 ++-- src/pages/app/components/HelpUs/index.tsx | 20 +++++---- .../app/components/RuleMerge/AlertInfo.tsx | 41 ------------------- src/pages/app/components/RuleMerge/index.tsx | 25 +++++++++-- src/pages/app/components/Whitelist/index.tsx | 19 ++++----- .../app/components/common/AlertLines.tsx | 20 +++++++++ 6 files changed, 67 insertions(+), 66 deletions(-) delete mode 100644 src/pages/app/components/RuleMerge/AlertInfo.tsx create mode 100644 src/pages/app/components/common/AlertLines.tsx diff --git a/package.json b/package.json index 3d29a2eb8..d29c8719e 100644 --- a/package.json +++ b/package.json @@ -40,11 +40,11 @@ "@types/chrome": "0.1.22", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.6.0", + "@types/node": "^24.7.0", "@types/punycode": "^2.1.4", - "@vue/babel-plugin-jsx": "^1.5.0", + "@vue/babel-plugin-jsx": "^2.0.1", "babel-loader": "^10.0.0", - "commitlint": "^20.0.0", + "commitlint": "^20.1.0", "css-loader": "^7.1.2", "decompress": "^4.2.1", "husky": "^9.1.7", @@ -54,7 +54,7 @@ "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.22.3", + "puppeteer": "^24.23.0", "sass": "^1.93.2", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", diff --git a/src/pages/app/components/HelpUs/index.tsx b/src/pages/app/components/HelpUs/index.tsx index 0e4a50d5a..2872b6e1d 100644 --- a/src/pages/app/components/HelpUs/index.tsx +++ b/src/pages/app/components/HelpUs/index.tsx @@ -1,10 +1,11 @@ import { createTabAfterCurrent } from "@api/chrome/tab" +import AlertLines from '@app/components/common/AlertLines' import { t } from "@app/locale" import { Pointer } from "@element-plus/icons-vue" import Box from "@pages/components/Box" import { CROWDIN_HOMEPAGE } from "@util/constant/url" -import { ElAlert, ElButton, ElCard, ElScrollbar } from "element-plus" -import { type FunctionalComponent, type StyleValue } from "vue" +import { ElButton, ElCard, ElScrollbar } from "element-plus" +import type { FunctionalComponent, StyleValue } from "vue" import ContentContainer from "../common/ContentContainer" import MemberList from "./MemberList" import ProgressList from "./ProgressList" @@ -15,12 +16,15 @@ const HelpUs: FunctionalComponent = () => ( - msg.helpUs.title)}> -
  • {t(msg => msg.helpUs.alert.l1)}
  • -
  • {t(msg => msg.helpUs.alert.l2)}
  • -
  • {t(msg => msg.helpUs.alert.l3)}
  • -
  • {t(msg => msg.helpUs.alert.l4)}
  • -
    + msg.helpUs.title} + lines={[ + msg => msg.helpUs.alert.l1, + msg => msg.helpUs.alert.l2, + msg => msg.helpUs.alert.l3, + msg => msg.helpUs.alert.l4, + ]} + /> {t(msg => msg.helpUs.button)} diff --git a/src/pages/app/components/RuleMerge/AlertInfo.tsx b/src/pages/app/components/RuleMerge/AlertInfo.tsx deleted file mode 100644 index fbfc89ee9..000000000 --- a/src/pages/app/components/RuleMerge/AlertInfo.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { t, tN } from "@app/locale" -import { PSL_HOMEPAGE } from "@util/constant/url" -import { ElAlert } from "element-plus" -import { type FunctionalComponent, type StyleValue } from "vue" - -const pslStyle: StyleValue = { - fontSize: "var(--el-alert-description-font-size)", - color: "var(--el-color-info)", - marginInline: "2px", -} - -const AlertInfo: FunctionalComponent = () => ( - msg.mergeRule.infoAlertTitle)} - style={{ padding: "15px 25px" } satisfies StyleValue} - closable={false} - > -
  • {t(msg => msg.mergeRule.infoAlert0)}
  • -
  • {t(msg => msg.mergeRule.infoAlert1)}
  • -
  • {t(msg => msg.mergeRule.infoAlert2)}
  • -
  • {t(msg => msg.mergeRule.infoAlert3)}
  • -
  • {t(msg => msg.mergeRule.infoAlert4)}
  • -
  • - { - tN(msg => msg.mergeRule.infoAlert5, { - psl: Public Suffix List - }) - } -
  • -
    -) - -export default AlertInfo \ No newline at end of file diff --git a/src/pages/app/components/RuleMerge/index.tsx b/src/pages/app/components/RuleMerge/index.tsx index 1f018f847..0097a322b 100644 --- a/src/pages/app/components/RuleMerge/index.tsx +++ b/src/pages/app/components/RuleMerge/index.tsx @@ -5,18 +5,37 @@ * https://opensource.org/licenses/MIT */ +import AlertLines from '@app/components/common/AlertLines' import Flex from "@pages/components/Flex" +import { PSL_HOMEPAGE } from '@util/constant/url' import { ElCard } from "element-plus" -import { type FunctionalComponent } from "vue" +import type { FunctionalComponent, StyleValue } from "vue" import ContentContainer from "../common/ContentContainer" -import AlertInfo from "./AlertInfo" import ItemList from "./ItemList" +const pslStyle: StyleValue = { + fontSize: "var(--el-alert-description-font-size)", + color: "var(--el-color-info)", + marginInline: "2px", +} + const RuleMerge: FunctionalComponent = () => ( - + msg.mergeRule.infoAlertTitle} + lines={[ + msg => msg.mergeRule.infoAlert0, + msg => msg.mergeRule.infoAlert1, + msg => msg.mergeRule.infoAlert2, + msg => msg.mergeRule.infoAlert3, + msg => msg.mergeRule.infoAlert4, + [msg => msg.mergeRule.infoAlert5, { + psl: Public Suffix List + }], + ]} + /> diff --git a/src/pages/app/components/Whitelist/index.tsx b/src/pages/app/components/Whitelist/index.tsx index f12c5bd39..d840e8b4c 100644 --- a/src/pages/app/components/Whitelist/index.tsx +++ b/src/pages/app/components/Whitelist/index.tsx @@ -5,9 +5,9 @@ * https://opensource.org/licenses/MIT */ -import { t } from "@app/locale" +import AlertLines from '@app/components/common/AlertLines' import Flex from "@pages/components/Flex" -import { ElAlert, ElCard } from "element-plus" +import { ElCard } from "element-plus" import { type FunctionalComponent } from "vue" import ContentContainer from "../common/ContentContainer" import WhitePanel from "./WhitePanel" @@ -16,14 +16,13 @@ const Whitelist: FunctionalComponent = () => ( - msg.whitelist.infoAlertTitle)} - style={{ padding: "15px 25px" }} - closable={false} - > -
  • {t(msg => msg.whitelist.infoAlert0)}
  • -
  • {t(msg => msg.whitelist.infoAlert1)}
  • -
    + msg.whitelist.infoAlertTitle} + lines={[ + msg => msg.whitelist.infoAlert0, + msg => msg.whitelist.infoAlert1, + ]} + />
    diff --git a/src/pages/app/components/common/AlertLines.tsx b/src/pages/app/components/common/AlertLines.tsx new file mode 100644 index 000000000..b84315579 --- /dev/null +++ b/src/pages/app/components/common/AlertLines.tsx @@ -0,0 +1,20 @@ +import { I18nKey, t, tN } from '@app/locale' +import { ElAlert } from 'element-plus' +import type { FunctionalComponent, StyleValue } from 'vue' + +const STYLE: StyleValue = { + padding: "15px 25px", +} + +const AlertLines: FunctionalComponent<{ + lines: (I18nKey | [I18nKey, param: any])[] + title: I18nKey +}> = ({ lines, title }) => ( + + {lines.map(l =>
  • {Array.isArray(l) ? tN(l[0], l[1]) : t(l)}
  • )} +
    +) + +AlertLines.displayName = 'AlertLines' + +export default AlertLines \ No newline at end of file From a62399d835d3e15b66749ac6158bfbd153f77994 Mon Sep 17 00:00:00 2001 From: sheepie Date: Fri, 10 Oct 2025 14:33:31 +0800 Subject: [PATCH 072/298] feat: retain cateogry filter for site management (#566) --- .../SiteManage/SiteManageFilter.tsx | 156 ++++++++---------- .../SiteManageTable/column/AliasColumn.tsx | 119 +++++++------ .../column/OperationColumn.tsx | 48 +++--- .../SiteManage/SiteManageTable/index.tsx | 29 ++-- src/pages/app/components/SiteManage/index.tsx | 38 ++--- .../components/SiteManage/useSiteManage.ts | 50 ++++++ .../common/category/CategorySelect/index.tsx | 77 +++++---- src/pages/hooks/useRequest.ts | 17 +- test-e2e/tracker/run-time.test.ts | 2 +- 9 files changed, 282 insertions(+), 254 deletions(-) create mode 100644 src/pages/app/components/SiteManage/useSiteManage.ts diff --git a/src/pages/app/components/SiteManage/SiteManageFilter.tsx b/src/pages/app/components/SiteManage/SiteManageFilter.tsx index 57c0fc564..7d18a6ecf 100644 --- a/src/pages/app/components/SiteManage/SiteManageFilter.tsx +++ b/src/pages/app/components/SiteManage/SiteManageFilter.tsx @@ -9,108 +9,86 @@ import InputFilterItem from "@app/components/common/filter/InputFilterItem" import { useCategories } from "@app/context" import { t } from "@app/locale" import { Connection, Delete, Grid, Plus } from "@element-plus/icons-vue" -import { useState } from "@hooks" import Flex from "@pages/components/Flex" -import { computed, defineComponent, type PropType, watch } from "vue" +import { computed, defineComponent, watch } from "vue" import DropdownButton, { type DropdownButtonItem } from "../common/DropdownButton" import CategoryFilter from "../common/filter/CategoryFilter" import MultiSelectFilterItem from "../common/filter/MultiSelectFilterItem" import { ALL_TYPES } from "./common" - -export type FilterOption = { - query?: string - types?: timer.site.Type[] - cateIds?: number[] -} +import { useSiteManageFilter } from './useSiteManage' type BatchOpt = 'change' | 'disassociate' | 'delete' -const _default = defineComponent({ - props: { - defaultValue: Object as PropType, - }, - emits: { - change: (_option: FilterOption) => true, - create: () => true, - batchDelete: () => true, - batchChangeCate: () => true, - batchDisassociate: () => true, - genNames: () => true, - }, - setup(props, ctx) { - const { categories } = useCategories() - - const defaultOption = props.defaultValue - const [query, setQuery] = useState(defaultOption?.query) - const [types, setTypes] = useState(defaultOption?.types) - - const cateDisabled = computed(() => !!types.value?.length && !types.value?.includes?.('normal')) - watch([cateDisabled], () => cateDisabled.value && setCateIds([])) +const _default = defineComponent<{ + onCreate: NoArgCallback + onBatchChangeCate: NoArgCallback + onBatchDisassociate: NoArgCallback + onBatchDelete: NoArgCallback +}>(props => { + const { categories } = useCategories() + const filter = useSiteManageFilter() - const [cateIds, setCateIds] = useState(defaultOption?.cateIds) + const cateDisabled = computed(() => { + const types = filter.types + return !!types?.length && !types?.includes?.('normal') + }) + watch(cateDisabled, () => cateDisabled.value && (filter.cateIds = [])) - watch(categories, () => { - const allCateIds = categories.value?.map(c => c.id) || [] - const newVal = cateIds.value?.filter(cid => allCateIds.includes(cid)) - // If selected category is deleted, then reset the value - newVal?.length !== cateIds.value?.length && setCateIds(newVal) - }) + watch(categories, () => { + const allCateIds = categories.value?.map(c => c.id) || [] + const newVal = filter.cateIds?.filter(cid => allCateIds.includes(cid)) + // If selected category is deleted, then reset the value + newVal?.length !== filter.cateIds?.length && (filter.cateIds = newVal) + }) - watch([query, types, cateIds], () => ctx.emit("change", { - query: query.value, - types: types.value, - cateIds: cateIds.value, - })) + const items: DropdownButtonItem[] = [{ + key: 'change', + label: t(msg => msg.siteManage.cate.batchChange), + icon: Grid, + onClick: props.onBatchChangeCate, + }, { + key: 'disassociate', + label: t(msg => msg.siteManage.cate.batchDisassociate), + icon: Connection, + onClick: props.onBatchDisassociate, + }, { + key: 'delete', + label: t(msg => msg.button.batchDelete), + icon: Delete, + onClick: props.onBatchDelete, + }] - const items: DropdownButtonItem[] = [{ - key: 'change', - label: t(msg => msg.siteManage.cate.batchChange), - icon: Grid, - onClick: () => ctx.emit('batchChangeCate'), - }, { - key: 'disassociate', - label: t(msg => msg.siteManage.cate.batchDisassociate), - icon: Connection, - onClick: () => ctx.emit('batchDisassociate'), - }, { - key: 'delete', - label: t(msg => msg.button.batchDelete), - icon: Delete, - onClick: () => ctx.emit('batchDelete'), - }] - - return () => ( - - - msg.item.host)} / ${t(msg => msg.siteManage.column.alias)}`} - onSearch={setQuery} - width={200} - /> - msg.siteManage.column.type)} - options={ALL_TYPES.map(type => ({ value: type, label: t(msg => msg.siteManage.type[type].name) }))} - defaultValue={types.value} - onChange={val => setTypes(val as timer.site.Type[])} - /> - - - - - msg.button.create)} - icon={Plus} - type="success" - onClick={() => ctx.emit("create")} - /> - + return () => ( + + + msg.item.host)} / ${t(msg => msg.siteManage.column.alias)}`} + onSearch={val => filter.query = val} + width={200} + /> + msg.siteManage.column.type)} + options={ALL_TYPES.map(type => ({ value: type, label: t(msg => msg.siteManage.type[type].name) }))} + defaultValue={filter.types} + onChange={val => filter.types = val as timer.site.Type[]} + /> + filter.cateIds = v} + /> + + + + msg.button.create)} + icon={Plus} + type="success" + onClick={props.onCreate} + /> - ) - } + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx b/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx index 87e319a8a..f127eb7a9 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/column/AliasColumn.tsx @@ -11,10 +11,11 @@ import { MagicStick } from "@element-plus/icons-vue" import Flex from "@pages/components/Flex" import siteService from "@service/site-service" import { getSuffix as getPslSuffix } from "@util/psl" -import { identifySiteKey } from "@util/site" -import { ElIcon, ElPopconfirm, ElTableColumn, ElText } from "element-plus" +import { identifySiteKey, SiteMap } from "@util/site" +import { ElIcon, ElMessage, ElPopconfirm, ElTableColumn, ElText } from "element-plus" import { toUnicode as punyCode2Unicode } from "punycode" import { defineComponent, type StyleValue } from "vue" +import { useSiteManageTable } from '../../useSiteManage' function cvt2Alias(part: string): string { let decoded = part @@ -38,59 +39,71 @@ export function genInitialAlias(site: timer.site.SiteInfo): string | undefined { return parts.reverse().map(cvt2Alias).join(' ') } -const _default = defineComponent({ - emits: { - rowAliasSaved: (_row: timer.site.SiteInfo) => true, - batchGenerate: () => true, - }, - setup: (_, ctx) => { - const handleChange = async (newAlias: string | undefined, row: timer.site.SiteInfo) => { - newAlias = newAlias?.trim?.() - row.alias = newAlias - if (newAlias) { - await siteService.saveAlias(row, newAlias) - } else { - await siteService.removeAlias(row) - } - ctx.emit("rowAliasSaved", row) +const AliasColumn = defineComponent<{}>(() => { + const { pagination, refresh } = useSiteManageTable() + + const handleChange = async (newAlias: string | undefined, row: timer.site.SiteInfo) => { + newAlias = newAlias?.trim?.() + row.alias = newAlias + if (newAlias) { + await siteService.saveAlias(row, newAlias) + } else { + await siteService.removeAlias(row) } + refresh() + } - return () => ( - ( - - {t(msg => msg.siteManage.column.alias)} - msg.siteManage.genAliasConfirmMsg)} - width={400} - onConfirm={() => ctx.emit('batchGenerate')} - v-slots={{ - reference: () => ( - - - - - - - - ) - }} - /> - - ), - default: ({ row }: { row: timer.site.SiteInfo }) => handleChange(val, row)} - /> - }} - /> - ) + const handleBatchGenerate = async () => { + let data = pagination.value?.list + if (!data?.length) { + return ElMessage.info("No data") + } + const toSave = new SiteMap() + const items = await siteService.batchSelect(data) + items.filter(i => !i.alias).forEach(site => { + const newAlias = genInitialAlias(site) + newAlias && toSave.put(site, newAlias) + }) + await siteService.batchSaveAliasNoRewrite(toSave) + refresh() + ElMessage.success(t(msg => msg.operation.successMsg)) } + + return () => ( + ( + + {t(msg => msg.siteManage.column.alias)} + msg.siteManage.genAliasConfirmMsg)} + width={400} + onConfirm={handleBatchGenerate} + v-slots={{ + reference: () => ( + + + + + + + + ) + }} + /> + + ), + default: ({ row }: { row: timer.site.SiteInfo }) => handleChange(val, row)} + /> + }} + /> + ) }) -export default _default \ No newline at end of file +export default AliasColumn \ No newline at end of file diff --git a/src/pages/app/components/SiteManage/SiteManageTable/column/OperationColumn.tsx b/src/pages/app/components/SiteManage/SiteManageTable/column/OperationColumn.tsx index 6e40247b0..202495fac 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/column/OperationColumn.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/column/OperationColumn.tsx @@ -10,29 +10,29 @@ import { Delete } from "@element-plus/icons-vue" import { type ElTableRowScope } from "@pages/element-ui/table" import siteService from "@service/site-service" import { ElTableColumn } from "element-plus" -import type { FunctionalComponent } from "vue" +import { defineComponent } from "vue" +import { useSiteManageTable } from '../../useSiteManage' -type Props = { onDelete?: ArgCallback } +const OperationColumn = defineComponent<{}>(() => { + const { refresh } = useSiteManageTable() + const handleConfirm = (key: timer.site.SiteKey) => siteService.remove(key).then(refresh).catch(() => { }) + return () => ( + msg.button.operation)} + align="center" + v-slots={ + ({ row }: ElTableRowScope) => ( + msg.button.delete)} + confirmText={t(msg => msg.siteManage.deleteConfirmMsg, { host: row.host })} + onConfirm={() => handleConfirm(row)} + /> + )} + /> + ) +}) -const _default: FunctionalComponent = props => ( - msg.button.operation)} - align="center" - v-slots={ - ({ row }: ElTableRowScope) => ( - msg.button.delete)} - confirmText={t(msg => msg.siteManage.deleteConfirmMsg, { host: row.host })} - onConfirm={async () => { - await siteService.remove(row) - props.onDelete?.(row) - }} - /> - )} - /> -) - -export default _default \ No newline at end of file +export default OperationColumn \ No newline at end of file diff --git a/src/pages/app/components/SiteManage/SiteManageTable/index.tsx b/src/pages/app/components/SiteManage/SiteManageTable/index.tsx index ae4f47dde..1dcebc3f0 100644 --- a/src/pages/app/components/SiteManage/SiteManageTable/index.tsx +++ b/src/pages/app/components/SiteManage/SiteManageTable/index.tsx @@ -11,21 +11,16 @@ import { type ElTableRowScope } from "@pages/element-ui/table" import siteService from "@service/site-service" import { SiteMap } from "@util/site" import { ElMessage, ElSwitch, ElTable, ElTableColumn } from "element-plus" -import { defineComponent, toRaw } from "vue" +import { defineComponent } from "vue" import Category from "../../common/category/CategoryEditable" +import { useSiteManageTable } from '../useSiteManage' import AliasColumn, { genInitialAlias } from "./column/AliasColumn" import OperationColumn from "./column/OperationColumn" import TypeColumn from "./column/TypeColumn" -type Props = { - data?: timer.site.SiteInfo[] - onRowDelete?: ArgCallback - onRowModify?: ArgCallback - onAliasGenerated?: NoArgCallback - onSelectionChange?: ArgCallback -} +const _default = defineComponent<{}>(() => { + const { setSelected, refresh, pagination } = useSiteManageTable() -const _default = defineComponent(props => { const handleIconError = async (row: timer.site.SiteInfo) => { await siteService.removeIconUrl(row) row.iconUrl = undefined @@ -35,11 +30,11 @@ const _default = defineComponent(props => { // Save await siteService.saveRun(row, val) row.run = val - props.onRowModify?.(toRaw(row)) + refresh() } const handleBatchGenerate = async () => { - let data = props.data + let data = pagination.value?.list if (!data?.length) { return ElMessage.info("No data") } @@ -50,16 +45,16 @@ const _default = defineComponent(props => { newAlias && toSave.put(site, newAlias) }) await siteService.batchSaveAliasNoRewrite(toSave) - props.onAliasGenerated?.() + refresh() ElMessage.success(t(msg => msg.operation.successMsg)) } return () => ( (props => { ) }} /> - + msg.siteManage.column.cate)} minWidth={140} @@ -113,9 +108,9 @@ const _default = defineComponent(props => { /> )} - + ) -}, { props: ['data', 'onRowDelete', 'onRowModify', 'onAliasGenerated', 'onSelectionChange'] }) +}) export default _default diff --git a/src/pages/app/components/SiteManage/index.tsx b/src/pages/app/components/SiteManage/index.tsx index 536f4055f..fb8c35805 100644 --- a/src/pages/app/components/SiteManage/index.tsx +++ b/src/pages/app/components/SiteManage/index.tsx @@ -7,30 +7,28 @@ import { t } from "@app/locale" import { Check, Close, WarnTriangleFilled } from "@element-plus/icons-vue" -import { useRequest, useState, useSwitch } from "@hooks" +import { useState, useSwitch } from "@hooks" import Flex from "@pages/components/Flex" -import siteService, { type SiteQueryParam } from "@service/site-service" +import siteService from "@service/site-service" import { supportCategory } from "@util/site" import { ElButton, ElDialog, ElForm, ElFormItem, ElMessage, ElMessageBox } from "element-plus" -import { computed, defineComponent, markRaw, ref } from "vue" +import { computed, defineComponent, markRaw, ref, type VNode } from "vue" import ContentContainer from "../common/ContentContainer" import Pagination from "../common/Pagination" import CategorySelect from "../common/category/CategorySelect" -import SiteManageFilter, { type FilterOption } from "./SiteManageFilter" +import SiteManageFilter from "./SiteManageFilter" import Modify, { type ModifyInstance } from './SiteManageModify' import SiteManageTable from "./SiteManageTable" +import { initSiteManage } from './useSiteManage' export default defineComponent(() => { - const [filterOption, setFilterOption] = useState() + const loadingTarget = ref() + const { + page, pagination, refresh, loading, + selected + } = initSiteManage(() => loadingTarget.value?.el as HTMLElement | undefined) const modify = ref() - const [page, setPage] = useState({ num: 1, size: 20 }) - const { data: pagination, refresh, loading } = useRequest(() => { - const { query: fuzzyQuery, cateIds, types } = filterOption.value || {} - const param: SiteQueryParam = { fuzzyQuery, cateIds, types } - return siteService.selectByPage(param, page.value) - }, { loadingTarget: '#site-manage-table-wrapper', deps: [filterOption, page] }) - const [selected, setSelected] = useState([]) const cateSupported = computed(() => selected?.value?.filter(supportCategory) || []) const [showCateChange, openCateChange, closeCateChange] = useSwitch(false) const [batchCate, setBatchCate] = useState() @@ -98,8 +96,6 @@ export default defineComponent(() => { return () => ( modify.value?.add?.()} onBatchChangeCate={handleChangeCate} onBatchDelete={handleBatchDelete} @@ -107,22 +103,16 @@ export default defineComponent(() => { /> ), content: () => <> - + - + { page.num = val.num, page.size = val.size }} /> diff --git a/src/pages/app/components/SiteManage/useSiteManage.ts b/src/pages/app/components/SiteManage/useSiteManage.ts new file mode 100644 index 000000000..68a39cbbb --- /dev/null +++ b/src/pages/app/components/SiteManage/useSiteManage.ts @@ -0,0 +1,50 @@ +import { RequestOption, useLocalStorage, useProvide, useProvider, useRequest, useState } from '@hooks' +import siteService, { type SiteQueryParam } from '@service/site-service' +import { type Reactive, reactive, type ShallowRef, watch } from 'vue' + +type FilterOption = { + query?: string + types?: timer.site.Type[] + cateIds?: number[] +} + +type CacheValue = { + cateIds?: number[] +} + +type Context = { + pagination: ShallowRef | undefined> + filter: Reactive + selected: ShallowRef + setSelected: ArgCallback + refresh: NoArgCallback +} + +const NAMESPACE = 'site-manage' + +export const initSiteManage = (loadingTarget: RequestOption['loadingTarget']) => { + const [cache, setCache] = useLocalStorage('site-manage-filter') + + const filter = reactive({ cateIds: cache?.cateIds }) + watch(() => filter.cateIds, cateIds => setCache({ cateIds })) + + const page = reactive({ num: 1, size: 20 }) + const [selected, setSelected] = useState([]) + + const { data: pagination, refresh, loading } = useRequest(() => { + const { query: fuzzyQuery, cateIds, types } = filter + const param: SiteQueryParam = { fuzzyQuery, cateIds, types } + return siteService.selectByPage(param, page) + }, { loadingTarget, deps: [() => filter, () => page] }) + + useProvide(NAMESPACE, { pagination, filter, selected, setSelected, refresh }) + + return { + pagination, refresh, loading, + selected, page, + } +} + +export const useSiteManageFilter = () => useProvider(NAMESPACE, 'filter').filter + +export const useSiteManageTable = () => useProvider(NAMESPACE, 'pagination', 'refresh', 'setSelected') \ No newline at end of file diff --git a/src/pages/app/components/common/category/CategorySelect/index.tsx b/src/pages/app/components/common/category/CategorySelect/index.tsx index d99bde0fb..f229a9159 100644 --- a/src/pages/app/components/common/category/CategorySelect/index.tsx +++ b/src/pages/app/components/common/category/CategorySelect/index.tsx @@ -1,6 +1,7 @@ import { useCategories } from "@app/context" +import { CATE_NOT_SET_ID } from '@util/site' import { ElOption, ElSelect, type SelectInstance } from "element-plus" -import { defineComponent, type PropType, ref } from "vue" +import { defineComponent, ref } from "vue" import OptionItem from "./OptionItem" import SelectFooter from "./SelectFooter" @@ -8,45 +9,43 @@ export type CategorySelectInstance = { openOptions: () => void } -const CategorySelect = defineComponent({ - props: { - modelValue: Number, - size: String as PropType<"small" | "">, - width: String, - clearable: Boolean, - }, - emits: { - visibleChange: (_visible: boolean) => true, - change: (_newVal: number | undefined) => true, - }, - setup(props, ctx) { - const { categories } = useCategories() +type Props = { + modelValue: number | undefined + size?: "small" + width?: string + clearable?: boolean + onVisibleChange?: ArgCallback + onChange?: ArgCallback +} + +const CategorySelect = defineComponent((props, ctx) => { + const { categories } = useCategories() - const selectRef = ref() - ctx.expose({ - openOptions: () => selectRef.value?.selectOption?.() - } satisfies CategorySelectInstance) + const selectRef = ref() + ctx.expose({ + openOptions: () => selectRef.value?.selectOption?.() + } satisfies CategorySelectInstance) - return () => ( - ctx.emit('change', val)} - onVisible-change={visible => ctx.emit('visibleChange', visible)} - style={{ width: props.width || '100%' }} - clearable={props.clearable} - onClear={() => ctx.emit('change', undefined)} - v-slots={{ footer: () => }} - > - {categories.value?.map(c => ( - - - - ))} - - ) - } -}) + return () => ( + ctx.emit('change', val)} + onVisible-change={visible => ctx.emit('visibleChange', visible)} + style={{ width: props.width || '100%' }} + clearable={props.clearable} + onClear={() => ctx.emit('change', undefined)} + emptyValues={[CATE_NOT_SET_ID, undefined]} + v-slots={{ footer: () => }} + > + {categories.value?.map(c => ( + + + + ))} + + ) +}, { props: ['clearable', 'modelValue', 'size', 'width', 'onVisibleChange', 'onChange'] }) export default CategorySelect \ No newline at end of file diff --git a/src/pages/hooks/useRequest.ts b/src/pages/hooks/useRequest.ts index 3f6306dab..fc0b9f429 100644 --- a/src/pages/hooks/useRequest.ts +++ b/src/pages/hooks/useRequest.ts @@ -1,5 +1,8 @@ import { ElLoadingService } from "element-plus" -import { onBeforeMount, onMounted, ref, watch, type Ref, type WatchSource } from "vue" +import { + onBeforeMount, onMounted, ref, shallowRef, watch, + type Ref, type ShallowRef, type WatchSource, +} from "vue" export type RequestOption = { manual?: boolean @@ -13,13 +16,13 @@ export type RequestOption = { } export type RequestResult = { - data: Ref - ts: Ref + data: ShallowRef + ts: ShallowRef refresh: (...p: P) => void refreshAsync: (...p: P) => Promise refreshAgain: () => void - loading: Ref - param: Ref

    + loading: ShallowRef + param: ShallowRef

    } const findLoadingEl = async (target: RequestOption['loadingTarget']): Promise => { @@ -57,7 +60,7 @@ export function useRequest

    ( deps, onSuccess, onError, } = option || {} - const data = ref(defaultValue) as Ref + const data = shallowRef(defaultValue) as ShallowRef const loading = ref(false) const param = ref

    () const ts = ref(Date.now()) @@ -89,7 +92,7 @@ export function useRequest

    ( hook(() => refresh(...defaultParam)) } if (deps && (!Array.isArray(deps) || deps?.length)) { - watch(deps, () => refresh(...defaultParam)) + watch(deps, () => refresh(...defaultParam), { deep: true }) } const refreshAgain = () => param.value && refresh(...param.value) return { data, ts, refresh, refreshAsync, refreshAgain, loading, param } diff --git a/test-e2e/tracker/run-time.test.ts b/test-e2e/tracker/run-time.test.ts index c89417183..f3b22b4ba 100644 --- a/test-e2e/tracker/run-time.test.ts +++ b/test-e2e/tracker/run-time.test.ts @@ -11,7 +11,7 @@ async function clickRunTimeChange(siteHost: string): Promise { await sitePage.keyboard.press('Enter') await sleep(.1) await sitePage.evaluate(() => { - const runTimeSwitch = document.querySelector('#site-manage-table-wrapper table > tbody > tr > td.el-table_1_column_7 .el-switch') + const runTimeSwitch = document.querySelector('table > tbody > tr > td.el-table_1_column_7 .el-switch') runTimeSwitch?.click() }) setTimeout(() => sitePage.close(), 200) From 4633189599ad2066c491444160121f0886a75914 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 12 Oct 2025 20:55:30 +0800 Subject: [PATCH 073/298] v3.6.6 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feb8287d1..fdb96a747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [3.6.6] - 2025-10-12 + +- Retain the category filter of site management + ## [3.6.5] - 2025-10-01 - Supported excluding subpages when setting restriction rules, thanks to [KHDKR](https://github.com/KHDKR) diff --git a/package.json b/package.json index d29c8719e..0faf49e29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "3.6.5", + "version": "3.6.6", "description": "Time tracker for browser", "homepage": "https://www.wfhg.cc", "scripts": { From 7b6da9753ec47ccc71fa71e61ab39e346cc2ae99 Mon Sep 17 00:00:00 2001 From: sheepie Date: Tue, 14 Oct 2025 00:43:51 +0800 Subject: [PATCH 074/298] feat: exclude sites for whitelist (#348) --- .gitignore | 2 + package.json | 8 +-- src/background/badge-manager.ts | 2 +- src/background/content-script-handler.ts | 2 +- .../migrator/whitelist-initializer.ts | 4 +- src/background/track-server.ts | 2 +- src/i18n/message/app/whitelist-resource.json | 24 ++++---- src/i18n/message/app/whitelist.ts | 1 + .../Limit/LimitModify/Sop/Step2.tsx | 8 +-- src/pages/app/components/Limit/LimitTest.tsx | 42 +++++--------- .../ReportTable/columns/OperationColumn.tsx | 2 +- .../Whitelist/WhitePanel/AddButton.tsx | 4 +- .../Whitelist/WhitePanel/WhiteInput.tsx | 57 +++++++++++++------ .../Whitelist/WhitePanel/WhiteItem.tsx | 17 +++++- .../components/Whitelist/WhitePanel/index.tsx | 24 ++++---- src/pages/app/components/Whitelist/index.tsx | 1 + .../app/components/common/AlertLines.tsx | 26 ++++++--- .../app/components/common/EditableTag.tsx | 4 +- src/service/components/virtual-site-holder.ts | 18 +----- src/service/components/whitelist-holder.ts | 53 ----------------- src/service/limit-service/index.ts | 2 +- src/service/whitelist/holder.ts | 36 ++++++++++++ src/service/whitelist/processor.ts | 33 +++++++++++ .../service.ts} | 2 +- src/util/constant/remain-host.ts | 4 +- src/util/limit.ts | 18 +++--- src/util/pattern.ts | 17 ++++++ test-e2e/common/base.ts | 8 ++- test-e2e/common/whitelist.ts | 10 +++- test/service/whitelist/processor.test.ts | 32 +++++++++++ tsconfig.json | 34 +++++------ 31 files changed, 299 insertions(+), 198 deletions(-) delete mode 100644 src/service/components/whitelist-holder.ts create mode 100644 src/service/whitelist/holder.ts create mode 100644 src/service/whitelist/processor.ts rename src/service/{whitelist-service.ts => whitelist/service.ts} (94%) create mode 100644 test/service/whitelist/processor.test.ts diff --git a/.gitignore b/.gitignore index 4277de201..ee659ba6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + node_modules .idea dist* diff --git a/package.json b/package.json index 0faf49e29..504ec8823 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,15 @@ "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.3", "@crowdin/crowdin-api-client": "^1.48.3", - "@rsdoctor/rspack-plugin": "^1.3.1", + "@rsdoctor/rspack-plugin": "^1.3.2", "@rspack/cli": "^1.5.8", "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", "@swc/jest": "^0.2.39", - "@types/chrome": "0.1.22", + "@types/chrome": "0.1.24", "@types/decompress": "^4.2.7", "@types/jest": "^30.0.0", - "@types/node": "^24.7.0", + "@types/node": "^24.7.2", "@types/punycode": "^2.1.4", "@vue/babel-plugin-jsx": "^2.0.1", "babel-loader": "^10.0.0", @@ -54,7 +54,7 @@ "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.23.0", + "puppeteer": "^24.24.0", "sass": "^1.93.2", "sass-loader": "^16.0.5", "style-loader": "^4.0.0", diff --git a/src/background/badge-manager.ts b/src/background/badge-manager.ts index 97ce3ac95..b1da2cb06 100644 --- a/src/background/badge-manager.ts +++ b/src/background/badge-manager.ts @@ -10,7 +10,7 @@ import { listTabs } from "@api/chrome/tab" import { getFocusedNormalWindow } from "@api/chrome/window" import statDatabase from "@db/stat-database" import optionHolder from "@service/components/option-holder" -import whitelistHolder from "@service/components/whitelist-holder" +import whitelistHolder from "@service/whitelist/holder" import { IS_ANDROID } from "@util/constant/environment" import { extractHostname, isBrowserUrl } from "@util/pattern" import { MILL_PER_HOUR, MILL_PER_MINUTE, MILL_PER_SECOND } from "@util/time" diff --git a/src/background/content-script-handler.ts b/src/background/content-script-handler.ts index 7c39b149e..d31277ac3 100644 --- a/src/background/content-script-handler.ts +++ b/src/background/content-script-handler.ts @@ -9,10 +9,10 @@ import { executeScript } from "@api/chrome/script" import { createTab } from "@api/chrome/tab" import { ANALYSIS_ROUTE, LIMIT_ROUTE } from "@app/router/constants" import optionHolder from "@service/components/option-holder" -import whitelistHolder from "@service/components/whitelist-holder" import limitService from "@service/limit-service" import siteService from "@service/site-service" import { saveTimelineEvent } from '@service/timeline-service' +import whitelistHolder from "@service/whitelist/holder" import { getAppPageUrl } from "@util/constant/url" import { extractFileHost, extractHostname } from "@util/pattern" import badgeManager from "./badge-manager" diff --git a/src/background/migrator/whitelist-initializer.ts b/src/background/migrator/whitelist-initializer.ts index bd237d2ab..5b6da1b43 100644 --- a/src/background/migrator/whitelist-initializer.ts +++ b/src/background/migrator/whitelist-initializer.ts @@ -1,9 +1,9 @@ -import whitelistService from "@service/whitelist-service" +import whitelistService from "@service/whitelist/service" import { type Migrator } from "./common" export default class WhitelistInitializer implements Migrator { onInstall(): void { - whitelistService.add('localhost:*') + whitelistService.add('localhost:*/**') } onUpdate(version: string): void { diff --git a/src/background/track-server.ts b/src/background/track-server.ts index 3fe1dac29..679804c8f 100644 --- a/src/background/track-server.ts +++ b/src/background/track-server.ts @@ -1,10 +1,10 @@ import { getTab, listTabs, sendMsg2Tab } from "@api/chrome/tab" import { getWindow } from "@api/chrome/window" import optionHolder from "@service/components/option-holder" -import whitelistHolder from "@service/components/whitelist-holder" import itemService, { type ItemIncContext } from "@service/item-service" import limitService from "@service/limit-service" import periodService from "@service/period-service" +import whitelistHolder from "@service/whitelist/holder" import { IS_ANDROID } from "@util/constant/environment" import { extractHostname } from "@util/pattern" import { formatTimeYMD, getStartOfDay, MILL_PER_DAY } from "@util/time" diff --git a/src/i18n/message/app/whitelist-resource.json b/src/i18n/message/app/whitelist-resource.json index e0a7e83ba..d4a925726 100644 --- a/src/i18n/message/app/whitelist-resource.json +++ b/src/i18n/message/app/whitelist-resource.json @@ -1,15 +1,16 @@ { "zh_CN": { - "addConfirmMsg": "{url} 加入白名单后,将不再统计该网站的数据", + "addConfirmMsg": "{url} 将被加入至白名单", "removeConfirmMsg": "{url} 将从白名单中移除", "duplicateMsg": "已存在白名单中", "infoAlertTitle": "你可以在这里配置网站白名单", "infoAlert0": "白名单内网站的上网时长和打开次数不会被统计", "infoAlert1": "白名单内网站的上网时间也不会被限制", + "infoAlert2": "您可以使用通配符 (*) 匹配多个站点,例如 *.example.com/**,并使用 + 作为前缀排除站点,例如 +need.example.com/**", "errorInput": "域名格式错误" }, "zh_TW": { - "addConfirmMsg": "將 {url} 加入白名單後,將停止統計此網站的瀏覽資料", + "addConfirmMsg": "{url} 將會被加入白名單。", "removeConfirmMsg": "確定要將 {url} 從白名單移除嗎?", "duplicateMsg": "此網址已存在於白名單中", "infoAlertTitle": "網站白名單設定", @@ -18,16 +19,17 @@ "errorInput": "網域名稱格式錯誤" }, "en": { - "addConfirmMsg": "{url} won't be counted after added into the whitelist any more.", + "addConfirmMsg": "{url} will be added to the whitelist.", "removeConfirmMsg": "{url} will be removed from the whitelist.", "duplicateMsg": "Duplicated", "infoAlertTitle": "You can whitelist sites on this page", "infoAlert0": "Whitelisted sites will not be counted", "infoAlert1": "Whitelisted sites will not be restricted", + "infoAlert2": "You can use a wildcards(*) to match multiple sites, such as *.example.com/**, and use + as a prefix to exclude sites, such as +need.example.com/**", "errorInput": "Invalid site URL" }, "ja": { - "addConfirmMsg": "{url} がホワイトリストに追加されると、このWebサイトの統計はカウントされなくなります。", + "addConfirmMsg": "{url} がホワイトリストに追加されます。", "removeConfirmMsg": "{url} はホワイトリストから削除されます", "duplicateMsg": "繰り返される", "infoAlertTitle": "このページでサイトのホワイトリストを設定できます", @@ -36,7 +38,7 @@ "errorInput": "無効なURL" }, "pt_PT": { - "addConfirmMsg": "{url} não será contabilizado após ser adicionado à lista de permissões.", + "addConfirmMsg": "{url} será adicionado à lista de permissões.", "removeConfirmMsg": "{url} será removido da lista de permissões.", "duplicateMsg": "Duplicado", "infoAlertTitle": "Pode adicionar sites à lista de permissões nesta página", @@ -45,7 +47,7 @@ "errorInput": "URL do site inválido" }, "uk": { - "addConfirmMsg": "{url} не враховуватиметься після додавання до білого списку.", + "addConfirmMsg": "{url} буде додано до білого списку.", "removeConfirmMsg": "{url} буде вилучено з білого списку.", "duplicateMsg": "Дублікат", "infoAlertTitle": "На цій сторінці ви можете налаштувати білий список сайтів", @@ -54,7 +56,7 @@ "errorInput": "Неприпустима URL-адреса сайту" }, "es": { - "addConfirmMsg": "{url} no se contará después de agregarlo a la lista blanca.", + "addConfirmMsg": "{url} se agregará a la lista blanca.", "removeConfirmMsg": "{url} será eliminado de la lista blanca.", "duplicateMsg": "Duplicado", "infoAlertTitle": "Puedes configurar una lista blanca de sitios en esta página", @@ -63,7 +65,7 @@ "errorInput": "URL del sitio inválida" }, "de": { - "addConfirmMsg": "{url} wird nach dem Hinzufügen zur Whitelist nicht mehr gezählt.", + "addConfirmMsg": "{url} wird zur Whitelist hinzugefügt.", "removeConfirmMsg": "{url} wird von der Whitelist entfernt.", "duplicateMsg": "Dupliziert", "infoAlertTitle": "Sie können eine Whitelist vonseiten auf dieser Seite festlegen", @@ -72,7 +74,7 @@ "errorInput": "Ungültige Seiten-URL" }, "fr": { - "addConfirmMsg": "{url} ne sera plus compté après avoir été ajouté à la liste blanche.", + "addConfirmMsg": "{url} sera ajouté à la liste blanche.", "removeConfirmMsg": "{url} sera supprimé de la liste blanche.", "duplicateMsg": "Doublon", "infoAlertTitle": "Vous pouvez ajouter des sites à la liste blanche sur cette page", @@ -81,7 +83,7 @@ "errorInput": "URL du site invalide" }, "ru": { - "addConfirmMsg": "{url} больше не будет учитываться после добавления в белый список.", + "addConfirmMsg": "{url} будет добавлен в белый список.", "removeConfirmMsg": "{url} будет удален из белого списка.", "duplicateMsg": "Дублированный", "infoAlertTitle": "Вы можете добавить сайты в белый список на этой странице", @@ -90,7 +92,7 @@ "errorInput": "Неверный URL-адрес сайта" }, "ar": { - "addConfirmMsg": "لن يتم احتساب {url} بعد إضافته إلى القائمة البيضاء بعد الآن.", + "addConfirmMsg": "سيتم إضافة {url} إلى القائمة البيضاء.", "removeConfirmMsg": "سيتم إزالة {url} من القائمة البيضاء.", "duplicateMsg": "مكررة", "infoAlertTitle": "يمكنك إضافة المواقع إلى القائمة البيضاء في هذه الصفحة", diff --git a/src/i18n/message/app/whitelist.ts b/src/i18n/message/app/whitelist.ts index 27b524728..54741f6f5 100644 --- a/src/i18n/message/app/whitelist.ts +++ b/src/i18n/message/app/whitelist.ts @@ -14,6 +14,7 @@ export type WhitelistMessage = { infoAlertTitle: string infoAlert0: string infoAlert1: string + infoAlert2: string errorInput: string } diff --git a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx index ade64e09f..d4755bf20 100644 --- a/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx +++ b/src/pages/app/components/Limit/LimitModify/Sop/Step2.tsx @@ -9,6 +9,7 @@ import { t } from "@app/locale" import { Delete, WarnTriangleFilled } from "@element-plus/icons-vue" import { useState } from "@hooks" import Flex from "@pages/components/Flex" +import { EXCLUDING_PREFIX } from '@util/constant/remain-host' import { cleanCond } from "@util/limit" import { ElButton, ElDivider, ElIcon, ElInput, ElLink, ElMessage, ElScrollbar, ElText, type ScrollbarInstance } from "element-plus" import { type StyleValue, defineComponent, ref } from "vue" @@ -46,10 +47,7 @@ const _default = defineComponent(() => { onKeydown={ev => (ev as KeyboardEvent)?.code === "Enter" && handleAdd()} v-slots={{ append: () => ( - + {t(msg => msg.button.add)} ) @@ -72,7 +70,7 @@ const _default = defineComponent(() => { padding='0 20px' style={{ backgroundColor: 'var(--el-fill-color)', borderRadius: 'var(--el-border-radius-large)' }} > - {url} + {url} handleRemove(idx)} /> ))} diff --git a/src/pages/app/components/Limit/LimitTest.tsx b/src/pages/app/components/Limit/LimitTest.tsx index 0f78ab604..ba1138e51 100644 --- a/src/pages/app/components/Limit/LimitTest.tsx +++ b/src/pages/app/components/Limit/LimitTest.tsx @@ -8,38 +8,28 @@ import { t } from "@app/locale" import { useState, useSwitch } from "@hooks" import limitService from "@service/limit-service" -import { AlertProps, ElAlert, ElButton, ElDialog, ElFormItem, ElInput } from "element-plus" +import { ElButton, ElDialog, ElFormItem, ElInput } from "element-plus" import { computed, defineComponent } from "vue" +import AlertLines, { type AlertLinesProps } from '../common/AlertLines' import { type TestInstance } from "./context" -function computeResultTitle(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): string { +function computeResult(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): AlertLinesProps { if (!url) { - return t(msg => msg.limit.message.inputTestUrl) + return { type: 'info', title: msg => msg.limit.message.inputTestUrl } } if (inputting) { - return t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.button.test) }) + const title = t(msg => msg.limit.message.clickTestButton, { buttonText: t(msg => msg.button.test) }) + return { type: 'info', title } } if (!matched?.length) { - return t(msg => msg.limit.message.noRuleMatched) + return { type: 'warning', title: msg => msg.limit.message.noRuleMatched } } else { - return t(msg => msg.limit.message.rulesMatched) - } -} - -function computeResultDesc(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): string[] { - if (!url || inputting || !matched?.length) { - return [] - } - return matched.map(m => m.name) -} - -type _ResultType = AlertProps['type'] - -function computeResultType(url: string | undefined, inputting: boolean, matched: timer.limit.Rule[]): _ResultType { - if (!url || inputting) { - return 'info' + return { + type: 'success', + title: msg => msg.limit.message.rulesMatched, + lines: matched.map(m => m.name) + } } - return matched?.length ? 'success' : 'warning' } const _default = defineComponent((_props, ctx) => { @@ -47,9 +37,7 @@ const _default = defineComponent((_props, ctx) => { const [matched, , clearMatched] = useState([]) const [visible, open, close] = useSwitch() const [urlInputting, startInput, endInput] = useSwitch(true) - const resultTitle = computed(() => computeResultTitle(url.value, urlInputting.value, matched.value)) - const resultType = computed(() => computeResultType(url.value, urlInputting.value, matched.value)) - const resultDesc = computed(() => computeResultDesc(url.value, urlInputting.value, matched.value)) + const result = computed(() => computeResult(url.value, urlInputting.value, matched.value)) const changeInput = (newVal?: string) => { startInput() @@ -88,9 +76,7 @@ const _default = defineComponent((_props, ctx) => { }} /> - - {resultDesc.value.map(desc =>

  • {desc}
  • )} - + ) }) diff --git a/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx b/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx index bb4923cc8..acb26fc05 100644 --- a/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx +++ b/src/pages/app/components/Report/ReportTable/columns/OperationColumn.tsx @@ -13,7 +13,7 @@ import { useRequest } from "@hooks" import { useTabGroups } from "@hooks/useTabGroups" import { locale } from "@i18n" import { type ElTableRowScope } from "@pages/element-ui/table" -import whitelistService from "@service/whitelist-service" +import whitelistService from "@service/whitelist/service" import { CATE_NOT_SET_ID } from "@util/site" import { isCate, isGroup, isNormalSite, isSite } from "@util/stat" import { ElButton, ElMessage, ElTableColumn } from "element-plus" diff --git a/src/pages/app/components/Whitelist/WhitePanel/AddButton.tsx b/src/pages/app/components/Whitelist/WhitePanel/AddButton.tsx index 339acf86f..310520863 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/AddButton.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/AddButton.tsx @@ -7,7 +7,7 @@ import { t } from "@app/locale" import { useState, useSwitch } from "@hooks" import { ElButton } from "element-plus" -import { defineComponent, type StyleValue } from "vue" +import { defineComponent, StyleValue } from "vue" import WhiteInput from './WhiteInput' type Props = { @@ -31,7 +31,7 @@ const _default = defineComponent(({ onSave }) => { end /> ) : ( - + {`+ ${t(msg => msg.button.create)}`} ) diff --git a/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx b/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx index 0d0d74ba4..8f5809940 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/WhiteInput.tsx @@ -6,26 +6,40 @@ */ import { t } from "@app/locale" -import { Check, Close } from "@element-plus/icons-vue" +import { Check, CirclePlus, Close } from "@element-plus/icons-vue" import { useRequest, useShadow } from "@hooks" import Box from "@pages/components/Box" +import Flex from '@pages/components/Flex' import siteService from "@service/site-service" -import { isRemainHost } from "@util/constant/remain-host" +import { EXCLUDING_PREFIX, isRemainHost } from "@util/constant/remain-host" import { isValidHost, judgeVirtualFast } from "@util/pattern" -import { ElButton, ElMessage, ElOption, ElSelect, ElTag } from "element-plus" +import { ElButton, ElIcon, ElMessage, ElOption, ElSelect, ElTag } from "element-plus" import { defineComponent, StyleValue } from "vue" -async function remoteSearch(query: string): Promise { +type SearchItem = timer.site.SiteKey & { + exclude?: boolean +} + +async function remoteSearch(query: string): Promise { + let exclude = false + if (query?.startsWith(EXCLUDING_PREFIX)) { + exclude = true + query = query.slice(1) + } if (!query) return [] - let sites: timer.site.SiteInfo[] = await siteService.selectAll({ fuzzyQuery: query }) - // Add local files + let sites: SearchItem[] = await siteService.selectAll({ fuzzyQuery: query }) const idx = sites.findIndex(s => s.host === query) - const target = idx > 0 - // Move to the first index - ? sites.splice(idx, 1)?.[0] - : { host: query, type: judgeVirtualFast(query) ? 'virtual' : 'normal' } satisfies timer.site.SiteKey - return [target, ...sites] + + const target = idx >= 0 + // Move found item to the front + ? sites.splice(idx, 1)[0] + // Or create a new one if not found + : { host: query, type: judgeVirtualFast(query) ? 'virtual' : 'normal' } satisfies SearchItem + const result = [target, ...sites] + + result.forEach(s => s.exclude = exclude) + return result } type Props = { @@ -42,7 +56,8 @@ const _default = defineComponent(props => { const handleSubmit = () => { const val = white.value if (!val) return - if (isRemainHost(val) || isValidHost(val) || judgeVirtualFast(val)) { + const host = val?.startsWith(EXCLUDING_PREFIX) ? val.substring(1) : val + if (isRemainHost(host) || isValidHost(host) || judgeVirtualFast(host)) { props.onSave?.(val) } else { ElMessage.warning(t(msg => msg.whitelist.errorInput)) @@ -67,11 +82,19 @@ const _default = defineComponent(props => { loading={searching.value} remoteMethod={search} > - {sites.value?.map(({ host, type }) => - {host} - - {t(msg => msg.siteManage.type.virtual?.name)?.toLocaleUpperCase?.()} - + {sites.value?.map(({ host, type, exclude }) => + + + + + {host} + + {t(msg => msg.siteManage.type.virtual?.name)?.toLocaleUpperCase?.()} + + )} diff --git a/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx b/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx index f63cca3c9..26db89160 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/WhiteItem.tsx @@ -5,8 +5,9 @@ * https://opensource.org/licenses/MIT */ -import EditableTag from "@app/components/common/EditableTag" +import EditableTag, { type EditableTagProps } from "@app/components/common/EditableTag" import { useShadow, useSwitch } from "@hooks" +import { EXCLUDING_PREFIX } from '@util/constant/remain-host' import { judgeVirtualFast } from "@util/pattern" import { computed, defineComponent } from "vue" import WhiteInput from "./WhiteInput" @@ -17,9 +18,19 @@ type Props = { onDelete: (white: string) => void } +function computeType(white: string): EditableTagProps['type'] { + if (white.startsWith(EXCLUDING_PREFIX)) { + return 'info' + } else if (judgeVirtualFast(white)) { + return 'warning' + } else { + return 'primary' + } +} + const _default = defineComponent(props => { const [white, , resetWhite] = useShadow(() => props.white) - const isVirtual = computed(() => !!white.value && judgeVirtualFast(white.value)) + const type = computed(() => computeType(white.value)) const [editing, openEditing, closeEditing] = useSwitch() const handleCancel = () => { @@ -38,7 +49,7 @@ const _default = defineComponent(props => { text={white.value} onEdit={openEditing} onClose={() => white.value && props.onDelete(white.value)} - type={isVirtual.value ? 'warning' : 'primary'} + type={type.value} /> ) }, { props: ['white', 'onChange', 'onDelete'] }) diff --git a/src/pages/app/components/Whitelist/WhitePanel/index.tsx b/src/pages/app/components/Whitelist/WhitePanel/index.tsx index 32e8dbe28..ac665afbf 100644 --- a/src/pages/app/components/Whitelist/WhitePanel/index.tsx +++ b/src/pages/app/components/Whitelist/WhitePanel/index.tsx @@ -5,33 +5,33 @@ * https://opensource.org/licenses/MIT */ import { t } from "@app/locale" -import { useRequest } from "@hooks" import Flex from "@pages/components/Flex" -import whitelistService from "@service/whitelist-service" +import whitelistService from "@service/whitelist/service" import { ElMessage, ElMessageBox } from "element-plus" -import { defineComponent } from "vue" +import { defineComponent, onBeforeMount, reactive } from "vue" import AddButton from './AddButton' import WhiteItem from './WhiteItem' const _default = defineComponent(() => { - const { data: whitelist } = useRequest(() => whitelistService.listAll(), { defaultValue: [] }) + const whitelist = reactive([]) + onBeforeMount(() => whitelistService.listAll().then(l => whitelist.push(...l))) const handleChanged = async (val: string, index: number): Promise => { - const duplicate = whitelist.value?.find?.((white, i) => white === val && i !== index) + const duplicate = whitelist.find((white, i) => white === val && i !== index) if (duplicate) { ElMessage.warning(t(msg => msg.whitelist.duplicateMsg)) // Reopen return false } - await whitelistService.remove(whitelist.value[index]) + await whitelistService.remove(whitelist[index]) await whitelistService.add(val) - whitelist.value[index] = val + whitelist[index] = val ElMessage.success(t(msg => msg.operation.successMsg)) return true } const handleAdd = async (val: string): Promise => { - const exists = whitelist.value?.some(item => item === val) + const exists = whitelist.some(item => item === val) if (exists) { ElMessage.warning(t(msg => msg.whitelist.duplicateMsg)) return false @@ -42,7 +42,7 @@ const _default = defineComponent(() => { return ElMessageBox.confirm(msg, title, { dangerouslyUseHTMLString: true }) .then(async () => { await whitelistService.add(val) - whitelist.value?.push(val) + whitelist.push(val) ElMessage.success(t(msg => msg.operation.successMsg)) return true }) @@ -57,15 +57,15 @@ const _default = defineComponent(() => { .then(() => whitelistService.remove(whiteItem)) .then(() => { ElMessage.success(t(msg => msg.operation.successMsg)) - const index = whitelist.value.indexOf(whiteItem) - index !== -1 && whitelist.value.splice(index, 1) + const index = whitelist.indexOf(whiteItem) + index !== -1 && whitelist.splice(index, 1) }) .catch(() => { }) } return () => ( - {whitelist.value?.map((white, index) => ( + {whitelist.map((white, index) => ( handleChanged(val, index)} diff --git a/src/pages/app/components/Whitelist/index.tsx b/src/pages/app/components/Whitelist/index.tsx index d840e8b4c..ba16cd5d0 100644 --- a/src/pages/app/components/Whitelist/index.tsx +++ b/src/pages/app/components/Whitelist/index.tsx @@ -21,6 +21,7 @@ const Whitelist: FunctionalComponent = () => ( lines={[ msg => msg.whitelist.infoAlert0, msg => msg.whitelist.infoAlert1, + msg => msg.whitelist.infoAlert2, ]} /> diff --git a/src/pages/app/components/common/AlertLines.tsx b/src/pages/app/components/common/AlertLines.tsx index b84315579..bcb195419 100644 --- a/src/pages/app/components/common/AlertLines.tsx +++ b/src/pages/app/components/common/AlertLines.tsx @@ -1,17 +1,29 @@ import { I18nKey, t, tN } from '@app/locale' -import { ElAlert } from 'element-plus' +import { type AlertProps, ElAlert } from 'element-plus' import type { FunctionalComponent, StyleValue } from 'vue' const STYLE: StyleValue = { padding: "15px 25px", } -const AlertLines: FunctionalComponent<{ - lines: (I18nKey | [I18nKey, param: any])[] - title: I18nKey -}> = ({ lines, title }) => ( - - {lines.map(l =>
  • {Array.isArray(l) ? tN(l[0], l[1]) : t(l)}
  • )} +export type AlertLinesProps = { + title: I18nKey | string + lines?: (I18nKey | [I18nKey, param: any] | string)[] + type?: AlertProps['type'] +} + +const AlertLines: FunctionalComponent = ({ lines, title, type }) => ( + + {lines?.map(l =>
  • { + Array.isArray(l) + ? tN(l[0], l[1]) + : (typeof l === 'string' ? l : t(l)) + }
  • )}
    ) diff --git a/src/pages/app/components/common/EditableTag.tsx b/src/pages/app/components/common/EditableTag.tsx index bc7ac0747..59a9fdbb2 100644 --- a/src/pages/app/components/common/EditableTag.tsx +++ b/src/pages/app/components/common/EditableTag.tsx @@ -3,7 +3,7 @@ import Flex from "@pages/components/Flex" import { ElTag, type TagProps } from "element-plus" import { defineComponent, h, useSlots, type StyleValue } from "vue" -type Props = PartialPick & { +export type EditableTagProps = PartialPick & { text?: string onEdit?: () => void closable?: boolean @@ -16,7 +16,7 @@ const EDIT_ICON_STYLE: StyleValue = { cursor: 'pointer', } -const EditableTag = defineComponent(props => { +const EditableTag = defineComponent(props => { const { default: textSlot } = useSlots() return () => ( { - if (seg === "**") { - return ".*" - } else { - return seg.replace?.(/\*/g, "[^/]*").replace(/\./g, "\\.") - } - }).join("/") - // "google.com/**" => google\.com.* - if (patternStr.endsWith("/.*")) { - patternStr = patternStr.substring(0, patternStr.length - 3) + ".*" - } - - return new RegExp("^(.+://)?" + patternStr + "/?(\\?.*)?$") -} +import { compileAntPattern } from '@util/pattern' /** * The singleton implementation of virtual sites holder diff --git a/src/service/components/whitelist-holder.ts b/src/service/components/whitelist-holder.ts deleted file mode 100644 index 6b6ebfec2..000000000 --- a/src/service/components/whitelist-holder.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) 2021 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import whitelistDatabase from "@db/whitelist-database" -import { judgeVirtualFast } from "@util/pattern" -import { compileAntPattern } from "./virtual-site-holder" - -/** - * The singleton implementation of whitelist holder - */ -class WhitelistHolder { - private host: string[] = [] - private virtual: RegExp[] = [] - private postHandlers: (() => void)[] - - constructor() { - whitelistDatabase.selectAll().then(list => this.setWhitelist(list)) - whitelistDatabase.addChangeListener((whitelist: string[]) => { - this.setWhitelist(whitelist) - this.postHandlers.forEach(handler => handler()) - }) - this.postHandlers = [] - } - - private setWhitelist(whitelist: string[]) { - const host: string[] = [] - const virtual: RegExp[] = [] - whitelist?.forEach(white => { - if (!white) return - if (judgeVirtualFast(white)) { - virtual.push(compileAntPattern(white)) - } else { - host.push(white) - } - }) - this.host = host - this.virtual = virtual - } - - addPostHandler(handler: () => void) { - this.postHandlers.push(handler) - } - - contains(host: string, url: string): boolean { - return this.host?.includes(host) || this.virtual?.some(r => r.test(url)) - } -} - -export default new WhitelistHolder() \ No newline at end of file diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts index 435925bbc..09d81b4ce 100644 --- a/src/service/limit-service/index.ts +++ b/src/service/limit-service/index.ts @@ -9,10 +9,10 @@ import { listTabs, sendMsg2Tab } from "@api/chrome/tab" import db from "@db/limit-database" import optionHolder from "@service/components/option-holder" import weekHelper from "@service/components/week-helper" +import whitelistHolder from "@service/whitelist/holder" import { sum } from "@util/array" import { calcTimeState, hasLimited, isEnabledAndEffective, matches } from "@util/limit" import { formatTimeYMD, MILL_PER_MINUTE } from "@util/time" -import whitelistHolder from '../components/whitelist-holder' export type QueryParam = { filterDisabled: boolean diff --git a/src/service/whitelist/holder.ts b/src/service/whitelist/holder.ts new file mode 100644 index 000000000..3661fbff1 --- /dev/null +++ b/src/service/whitelist/holder.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import whitelistDatabase from "@db/whitelist-database" +import WhitelistProcessor from './processor' + +/** + * The singleton implementation of whitelist holder + */ +class WhitelistHolder { + private processor = new WhitelistProcessor() + private postHandlers: NoArgCallback[] + + constructor() { + whitelistDatabase.selectAll().then(list => this.processor.setWhitelist(list)) + whitelistDatabase.addChangeListener(whitelist => { + this.processor.setWhitelist(whitelist) + this.postHandlers.forEach(handler => handler()) + }) + this.postHandlers = [] + } + + addPostHandler(handler: () => void) { + this.postHandlers.push(handler) + } + + contains(host: string, url: string): boolean { + return this.processor.contains(host, url) + } +} + +export default new WhitelistHolder() \ No newline at end of file diff --git a/src/service/whitelist/processor.ts b/src/service/whitelist/processor.ts new file mode 100644 index 000000000..27b95b280 --- /dev/null +++ b/src/service/whitelist/processor.ts @@ -0,0 +1,33 @@ +import { EXCLUDING_PREFIX } from '@util/constant/remain-host' +import { compileAntPattern, judgeVirtualFast } from '@util/pattern' + +export default class WhitelistProcessor { + private host: string[] = [] + private virtual: RegExp[] = [] + private exclude: RegExp[] = [] + + setWhitelist(whitelist: string[]) { + const host: string[] = [] + const virtual: RegExp[] = [] + const exclude: RegExp[] = [] + whitelist.forEach(white => { + if (!white) return + if (white.startsWith(EXCLUDING_PREFIX)) { + const val = white.substring(1) + exclude.push(compileAntPattern(val)) + } else if (judgeVirtualFast(white)) { + virtual.push(compileAntPattern(white)) + } else { + host.push(white) + } + }) + this.host = host + this.virtual = virtual + this.exclude = exclude + } + + contains(host: string, url: string): boolean { + if (this.exclude.some(r => r.test(url))) return false + return this.host.includes(host) || this.virtual.some(r => r.test(url)) + } +} diff --git a/src/service/whitelist-service.ts b/src/service/whitelist/service.ts similarity index 94% rename from src/service/whitelist-service.ts rename to src/service/whitelist/service.ts index 5dd0b244c..b9d267378 100644 --- a/src/service/whitelist-service.ts +++ b/src/service/whitelist/service.ts @@ -6,7 +6,7 @@ */ import whitelistDatabase from "@db/whitelist-database" -import { log } from "../common/logger" +import { log } from '@src/common/logger' /** * Service of whitelist diff --git a/src/util/constant/remain-host.ts b/src/util/constant/remain-host.ts index 99242add3..ccfb91fc7 100644 --- a/src/util/constant/remain-host.ts +++ b/src/util/constant/remain-host.ts @@ -31,4 +31,6 @@ export const SUFFIX_HOST_MAP: Record = { const reg = /^__local_(.+)__$/ export function isRemainHost(host: string) { return reg.test(host) -} \ No newline at end of file +} + +export const EXCLUDING_PREFIX = '+' \ No newline at end of file diff --git a/src/util/limit.ts b/src/util/limit.ts index 3809372fb..67ff57b46 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -1,3 +1,4 @@ +import { EXCLUDING_PREFIX } from './constant/remain-host' import { getWeekDay, MILL_PER_MINUTE, MILL_PER_SECOND } from "./time" export const DELAY_MILL = 5 * MILL_PER_MINUTE @@ -18,8 +19,6 @@ const matchUrl = (cond: string, url: string): boolean => { return new RegExp(`^.*//${cond.split('*').join('.*')}`).test(url) } -const WHITE_PREFIX = '+' //Denotes an exclusion rule when used as a prefix in a condition string - /** * checks whether the provided URL matches the rule list (cond), following the exclusion rule priority * @param cond @@ -29,7 +28,7 @@ export function matches(cond: timer.limit.Item['cond'], url: string): boolean { let hit = false for (let i = cond.length - 1; i >= 0; i--) { const rule = cond[i] - if (rule.startsWith(WHITE_PREFIX)) { + if (rule.startsWith(EXCLUDING_PREFIX)) { if (matchUrl(rule.slice(1), url)) return false } else { hit = hit || matchUrl(rule, url) @@ -44,16 +43,17 @@ export function matches(cond: timer.limit.Item['cond'], url: string): boolean { * @param url */ export function matchCond(cond: timer.limit.Item['cond'], url: string): string[] { - const matchedNormalRules: string[] = []; + const matchedNormalRules: string[] = [] for (let i = cond.length - 1; i >= 0; i--) { - const rule = cond[i]; - if (rule.startsWith(WHITE_PREFIX)) { - if (matchUrl(rule.slice(1), url)) return []; //Immediately return an empty array if an exclusion rule is hit + const rule = cond[i] + if (rule.startsWith(EXCLUDING_PREFIX)) { + // Immediately return an empty array if an exclusion rule is hit + if (matchUrl(rule.slice(1), url)) return [] } else { - if (matchUrl(rule, url)) matchedNormalRules.push(rule); + if (matchUrl(rule, url)) matchedNormalRules.push(rule) } } - return matchedNormalRules; + return matchedNormalRules } export const meetLimit = (limit: number | undefined, value: number | undefined): boolean => { diff --git a/src/util/pattern.ts b/src/util/pattern.ts index f74f3c86c..655515fae 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -186,4 +186,21 @@ export function isHomepage(url: string) { export function escapeRegExp(s: string): string { if (!s) return '' return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') +} + +export function compileAntPattern(antPattern: string): RegExp { + const segments = antPattern.split('/') + let patternStr = segments.map(seg => { + if (seg === "**") { + return ".*" + } else { + return seg.replace?.(/\*/g, "[^/]*").replace(/\./g, "\\.") + } + }).join("/") + // "google.com/**" => google\.com.* + if (patternStr.endsWith("/.*")) { + patternStr = patternStr.substring(0, patternStr.length - 3) + ".*" + } + + return new RegExp("^(.+://)?" + patternStr + "/?(\\?.*)?$") } \ No newline at end of file diff --git a/test-e2e/common/base.ts b/test-e2e/common/base.ts index 259d7b100..81d044910 100644 --- a/test-e2e/common/base.ts +++ b/test-e2e/common/base.ts @@ -1,5 +1,6 @@ import { type Browser, launch, type Page } from "puppeteer" import { E2E_OUTPUT_PATH } from "../../rspack/constant" +import { removeAllWhitelist } from './whitelist' const USE_HEADLESS_PUPPETEER = !!process.env['USE_HEADLESS_PUPPETEER'] @@ -71,7 +72,12 @@ export async function launchBrowser(dirPath?: string): Promise { throw new Error('Failed to detect extension id') } - return new LaunchContextWrapper(browser, extensionId) + const context = new LaunchContextWrapper(browser, extensionId) + + // remove whitelist added by service_worker + await removeAllWhitelist(context) + + return context } export function sleep(seconds: number): Promise { diff --git a/test-e2e/common/whitelist.ts b/test-e2e/common/whitelist.ts index 75c881185..357e7afc5 100644 --- a/test-e2e/common/whitelist.ts +++ b/test-e2e/common/whitelist.ts @@ -11,10 +11,18 @@ export async function createWhitelist(context: LaunchContext, white: string) { await input?.focus() await whitePage.keyboard.type(white) await sleep(.4) - const selectItem = await whitePage.waitForSelector('.el-popper .el-select-dropdown li:nth-child(2)') + const selectItem = await whitePage.waitForSelector('.el-popper .el-select-dropdown li:nth-child(1)') await selectItem?.click() await whitePage.click('.el-button:nth-child(3)') const checkBtn = await whitePage.waitForSelector('.el-overlay.is-message-box .el-button.el-button--primary') await checkBtn?.click() setTimeout(() => whitePage.close(), 200) +} + +export async function removeAllWhitelist(context: LaunchContext) { + const whitePage = await context.openAppPage('/additional/whitelist') + await whitePage.evaluate(async () => { + await chrome.storage.local.remove('__timer__WHITELIST') + }) + await whitePage.close() } \ No newline at end of file diff --git a/test/service/whitelist/processor.test.ts b/test/service/whitelist/processor.test.ts new file mode 100644 index 000000000..4a5de79c6 --- /dev/null +++ b/test/service/whitelist/processor.test.ts @@ -0,0 +1,32 @@ +import WhitelistProcessor from '@service/whitelist/processor' + +describe('whitelist-holder', () => { + let processor: WhitelistProcessor + + beforeEach(() => processor = new WhitelistProcessor()) + + test('normal', () => { + processor.setWhitelist([ + "www.google.com", + "github.com", + ]) + expect(processor.contains("github.com", "")).toBeTruthy() + expect(processor.contains("www.github.com", "")).toBeFalsy() + expect(processor.contains("www.google.com", "http://www.google.com/search")).toBeTruthy() + }) + + test('wildcards', () => { + processor.setWhitelist([ + "www.github.com", + "*.google.com/**", + "+www.google.com/**", + ]) + expect(processor.contains("google.com", "https://google.com/")).toBeFalsy() + expect(processor.contains("", "http://map.google.com/search")).toBeTruthy() + + // virtual sites only use url + expect(processor.contains("www.google.com", "https://foo.bar.google.com/")).toBeTruthy() + // hit "+www.google.com/**" + expect(processor.contains("www.google.com", "https://www.google.com/")).toBeFalsy() + }) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a0e34a5f4..e743153aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { - "baseUrl": "./", - "module": "commonjs", + "module": "esnext", "target": "esnext", "lib": [ "ES2023" @@ -16,51 +15,52 @@ "resolveJsonModule": true, "importHelpers": true, "skipLibCheck": true, + "moduleResolution": "bundler", "paths": { "@api/*": [ - "src/api/*" + "./src/api/*" ], "@cs/*": [ - "src/content-script/*" + "./src/content-script/*" ], "@app/*": [ - "src/pages/app/*" + "./src/pages/app/*" ], "@popup/*": [ - "src/pages/popup/*" + "./src/pages/popup/*" ], "@side/*": [ - "src/pages/side/*" + "./src/pages/side/*" ], "@hooks/*": [ - "src/pages/hooks/*" + "./src/pages/hooks/*" ], "@hooks": [ - "src/pages/hooks/index" + "./src/pages/hooks/index" ], "@pages/*": [ - "src/pages/*" + "./src/pages/*" ], "@db/*": [ - "src/database/*" + "./src/database/*" ], "@service/*": [ - "src/service/*" + "./src/service/*" ], "@util/*": [ - "src/util/*" + "./src/util/*" ], "@i18n/*": [ - "src/i18n/*" + "./src/i18n/*" ], "@i18n": [ - "src/i18n/index" + "./src/i18n/index" ], "@src/*": [ - "src/*" + "./src/*" ], "*": [ - "types/*" + "./types/*" ] } }, From daad2c06ca5308a215f068223ad3a119fd9a5bf4 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Tue, 14 Oct 2025 10:05:34 +0800 Subject: [PATCH 075/298] ci: create tsconfig.node.json --- .github/workflows/crowdin-export.yml | 2 +- .github/workflows/crowdin-sync.yml | 4 ++-- tsconfig.node.json | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 tsconfig.node.json diff --git a/.github/workflows/crowdin-export.yml b/.github/workflows/crowdin-export.yml index 96831f7d5..afd0cded5 100644 --- a/.github/workflows/crowdin-export.yml +++ b/.github/workflows/crowdin-export.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: npm install - name: Export translations - run: ts-node ./script/crowdin/export-translation.ts + run: ts-node --project ./tsconfig.node.json ./script/crowdin/export-translation.ts - name: Test typescript uses: icrawl/action-tsc@v1 - name: Create Pull Request diff --git a/.github/workflows/crowdin-sync.yml b/.github/workflows/crowdin-sync.yml index 0035a5d2f..805105ffb 100644 --- a/.github/workflows/crowdin-sync.yml +++ b/.github/workflows/crowdin-sync.yml @@ -16,6 +16,6 @@ jobs: - name: Install dependencies run: npm install - name: Sync source - run: ts-node ./script/crowdin/sync-source.ts + run: ts-node --project ./tsconfig.node.json ./script/crowdin/sync-source.ts - name: Sync translations - run: ts-node ./script/crowdin/sync-translation.ts + run: ts-node --project ./tsconfig.node.json ./script/crowdin/sync-translation.ts diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 000000000..1b7a59159 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs" + } +} From 23bcc911ccc36821a3e9c466bad07b8dc588d03a Mon Sep 17 00:00:00 2001 From: zhy Date: Tue, 14 Oct 2025 18:57:12 +0800 Subject: [PATCH 076/298] fix: fix about error --- src/pages/app/components/About/DescLink.tsx | 2 +- .../app/components/About/Description.tsx | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/app/components/About/DescLink.tsx b/src/pages/app/components/About/DescLink.tsx index 26590c20e..91bdd6a03 100644 --- a/src/pages/app/components/About/DescLink.tsx +++ b/src/pages/app/components/About/DescLink.tsx @@ -10,7 +10,7 @@ const _default = defineComponent<{ href?: string, icon?: Icon }>(props => { return () => ( {icon ?
    : null} - {!!default_ && h(default_)} + {default_ ? h(default_) : href ?? ''} ) }, { props: ['href', 'icon'] }) diff --git a/src/pages/app/components/About/Description.tsx b/src/pages/app/components/About/Description.tsx index 844e14f6f..df1bea655 100644 --- a/src/pages/app/components/About/Description.tsx +++ b/src/pages/app/components/About/Description.tsx @@ -17,7 +17,7 @@ import { SOURCE_CODE_PAGE, } from "@util/constant/url" import { type ComponentSize, ElCard, ElDescriptions, ElDescriptionsItem, ElDivider, ElText } from "element-plus" -import { computed, defineComponent } from "vue" +import { computed, defineComponent, reactive } from "vue" import DescLink from "./DescLink" import "./description.sass" import InstallationLink from "./InstallationLink" @@ -32,12 +32,18 @@ const computeSize = (mediaSize: MediaSize): ComponentSize => { } } -const _default = defineComponent(() => { +const _default = defineComponent<{}>(() => { const feedbackUrl = FEEDBACK_QUESTIONNAIRE[locale] || GITHUB_ISSUE_ADD const mediaSize = useMediaSize() const column = computed(() => mediaSize.value <= MediaSize.md ? 1 : 2) - const isXs = computed(() => mediaSize.value === MediaSize.xs) const size = computed(() => computeSize(mediaSize.value)) + const pages = reactive({ + homepage: HOMEPAGE, + privacy: PRIVACY_PAGE, + sourceCode: SOURCE_CODE_PAGE, + changeLog: CHANGE_LOG_PAGE, + email: AUTHOR_EMAIL, + }) return () => ( @@ -54,18 +60,14 @@ const _default = defineComponent(() => { msg.about.label.website)} labelAlign="right"> - {HOMEPAGE} + {pages.homepage} msg.about.label.privacy)} labelAlign="right"> - - {PRIVACY_PAGE} - + msg.base.sourceCode)} labelAlign="right"> - - {SOURCE_CODE_PAGE} - + msg.about.label.license)} labelAlign="right"> @@ -73,12 +75,10 @@ const _default = defineComponent(() => { msg.base.changeLog)} labelAlign="right"> - - {CHANGE_LOG_PAGE} - + msg.about.label.support)} labelAlign="right"> - {AUTHOR_EMAIL} + {pages.email} msg.about.label.installation)} labelAlign="right"> From 0a55f8a85e8022bde7cdc441c2948c828fbb6834 Mon Sep 17 00:00:00 2001 From: sheepie Date: Thu, 16 Oct 2025 10:04:24 +0800 Subject: [PATCH 077/298] refactor: option codes (#576) --- package.json | 13 ++- rspack/rspack.common.ts | 11 +-- src/background/icon-and-alias-collector.ts | 12 +-- src/i18n/message/app/option-resource.json | 84 +++++-------------- src/i18n/message/app/option.ts | 7 +- .../Option/components/AccessibilityOption.tsx | 19 ++--- .../AppearanceOption/DarkModeInput.tsx | 5 +- .../components/AppearanceOption/index.tsx | 25 +++--- .../Option/components/BackupOption/index.tsx | 19 +++-- .../Option/components/LimitOption/index.tsx | 6 +- .../Option/components/OptionItem.tsx | 51 +++++++---- .../Option/components/OptionLines.tsx | 59 +++++++++++++ .../Option/components/OptionTag.tsx | 18 ++-- .../Option/components/OptionTooltip.tsx | 16 ++-- .../Option/components/PopupOption.tsx | 29 ++----- .../Option/components/StatisticsOption.tsx | 20 +---- .../popup/components/Header/LangSelect.tsx | 2 +- src/util/constant/option.ts | 1 - types/timer/option.d.ts | 6 -- 19 files changed, 190 insertions(+), 213 deletions(-) create mode 100644 src/pages/app/components/Option/components/OptionLines.tsx diff --git a/package.json b/package.json index 504ec8823..94dcd316e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/preset-env": "^7.28.3", "@crowdin/crowdin-api-client": "^1.48.3", - "@rsdoctor/rspack-plugin": "^1.3.2", + "@rsdoctor/rspack-plugin": "^1.3.3", "@rspack/cli": "^1.5.8", "@rspack/core": "^1.5.8", "@swc/core": "^1.13.5", @@ -51,21 +51,20 @@ "jest": "^30.2.0", "jest-environment-jsdom": "^30.2.0", "jest-junit": "^16.0.0", + "jszip": "^3.10.1", "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "postcss-rtlcss": "^5.7.1", - "puppeteer": "^24.24.0", + "puppeteer": "^24.25.0", "sass": "^1.93.2", "sass-loader": "^16.0.5", - "style-loader": "^4.0.0", "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "5.9.3", - "url-loader": "^4.1.1" + "typescript": "5.9.3" }, "optionalDependencies": { - "web-ext": "^8.10.0" + "web-ext": "^9.0.0" }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", @@ -75,7 +74,7 @@ "js-base64": "^3.7.8", "punycode": "^2.3.1", "vue": "^3.5.22", - "vue-router": "^4.5.1" + "vue-router": "^4.6.2" }, "engines": { "node": ">=20" diff --git a/rspack/rspack.common.ts b/rspack/rspack.common.ts index 36128a728..44903245a 100644 --- a/rspack/rspack.common.ts +++ b/rspack/rspack.common.ts @@ -93,21 +93,16 @@ const staticOptions: Configuration = { test: /\.css$/, use: [CssExtractRspackPlugin.loader, 'css-loader', POSTCSS_LOADER_CONF], }, { - test: /\.sc|ass$/, + test: /\.s[ac]ss$/, use: [CssExtractRspackPlugin.loader, 'css-loader', POSTCSS_LOADER_CONF, 'sass-loader'] }, { test: /\.(jpg|jpeg|png|woff|woff2|eot|ttf|svg)$/, - exclude: /node_modules/, - use: ['url-loader'] - }, { - test: /\.m?js$/, - exclude: /(node_modules)/, - use: ['babel-loader'] + type: 'asset/resource' } ] }, resolve: { - extensions: ['.ts', '.tsx', ".js", '.css', '.scss', '.sass'], + extensions: ['.ts', '.tsx', '.js', '.css', '.scss', '.sass'], tsConfig: join(__dirname, '..', 'tsconfig.json'), }, optimization: { diff --git a/src/background/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index 3f54cc734..c3a65cc69 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -6,20 +6,11 @@ */ import { getTab } from "@api/chrome/tab" -import optionDatabase from "@db/option-database" import siteService from "@service/site-service" import { IS_ANDROID, IS_CHROME, IS_SAFARI } from "@util/constant/environment" -import { defaultStatistics } from "@util/constant/option" import { extractHostname, isBrowserUrl, isHomepage } from "@util/pattern" import { extractSiteName } from "@util/site" -const storage: chrome.storage.StorageArea = chrome.storage.local - -let collectAliasEnabled = defaultStatistics().collectSiteName -const setCollectAliasEnabled = (opt: timer.option.AllOption) => collectAliasEnabled = opt.collectSiteName -optionDatabase.getOption().then(setCollectAliasEnabled) -optionDatabase.addOptionChangeListener(setCollectAliasEnabled) - function isUrl(title: string) { return title.startsWith('https://') || title.startsWith('http://') || title.startsWith('ftp://') } @@ -45,8 +36,7 @@ async function processTabInfo(tab: ChromeTab): Promise { IS_CHROME && /^localhost(:.+)?/.test(host) && (favIconUrl = undefined) const siteKey: timer.site.SiteKey = { host, type: 'normal' } favIconUrl && await siteService.saveIconUrl(siteKey, favIconUrl) - collectAliasEnabled - && !isBrowserUrl(url) + !isBrowserUrl(url) && isHomepage(url) && await collectAlias(siteKey, title) } diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index 3d1b56f38..948e2f754 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -2,10 +2,11 @@ "zh_CN": { "yes": "是", "no": "否", + "followBrowser": "跟随浏览器", "popup": { "title": "弹窗页", "max": "只显示前 {input} 条数据,剩下的条目合并显示", - "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名" + "displaySiteName": "{input} 显示时是否使用网站名称来代替域名" }, "appearance": { "title": "外观", @@ -18,7 +19,6 @@ "badgeTextContent": "当前网站的今日浏览时长", "locale": { "label": "语言设置 {input}", - "default": "跟随浏览器", "changeConfirm": "语言设置成功,请刷新页面!", "reloadButton": "刷新" }, @@ -30,7 +30,6 @@ "darkMode": { "label": "夜间模式 {input}", "options": { - "default": "跟随浏览器", "on": "始终开启", "off": "始终关闭", "timed": "定时开启" @@ -48,9 +47,6 @@ "tabGroupsPermGrant": "该功能需要授予相关权限", "localFileTime": "阅读本地文件的时间", "localFilesInfo": "支持 PDF、图片、txt 以及 json 等格式", - "collectSiteName": "{input} 访问网站主页时,是否自动收集 {siteName} {siteNameUsage}", - "siteName": "网站的名称", - "siteNameUsage": "数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称", "fileAccessDisabled": "目前不允许访问文件网址,请先在管理界面开启", "fileAccessFirefox": "很抱歉,该功能在 Firefox 中不支持", "weekStart": "每周的第一天 {input}", @@ -141,10 +137,11 @@ "zh_TW": { "yes": "是", "no": "否", + "followBrowser": "跟隨瀏覽器", "popup": { "title": "彈出視窗", "max": "僅顯示前 {input} 筆資料,其餘項目合併", - "displaySiteName": "{input} 是否使用 {siteName} 取代網域" + "displaySiteName": "{input} 顯示時是否使用網站名稱而非域名" }, "appearance": { "title": "外觀設定", @@ -157,7 +154,6 @@ "badgeTextContent": "當前網站今日瀏覽時長", "locale": { "label": "語言設定 {input}", - "default": "跟隨瀏覽器", "changeConfirm": "語言設定已更新,請重新整理頁面!", "reloadButton": "重新整理" }, @@ -169,7 +165,6 @@ "darkMode": { "label": "深色模式 {input}", "options": { - "default": "跟隨瀏覽器", "on": "始終開啟", "off": "始終關閉", "timed": "定時開啟" @@ -187,9 +182,6 @@ "countTabGroup": "{input} 是否追蹤分頁群組的時間 {info}", "tabGroupInfo": "刪除分頁群組時,相關的時間追蹤數據也會一併清除。", "tabGroupsPermGrant": "此功能需要相關權限才能運作", - "collectSiteName": "{input} 造訪網站首頁時自動收集 {siteName} {siteNameUsage}", - "siteName": "網站名稱", - "siteNameUsage": "資料僅儲存於本地,將取代網域名稱提升辨識度,您可自訂各網站名稱", "fileAccessDisabled": "目前不允許存取檔案 URL,請至管理頁面啟用", "fileAccessFirefox": "抱歉,Firefox 不支援此功能", "weekStart": "每週起始日 {input}", @@ -279,10 +271,11 @@ "en": { "yes": "Yes", "no": "No", + "followBrowser": "Follow browser", "popup": { "title": "Popup Page", "max": "Show the first {input} data items", - "displaySiteName": "{input} Whether to display {siteName} instead of URL" + "displaySiteName": "{input} Whether to display the website name instead of URL" }, "appearance": { "title": "Appearance", @@ -295,7 +288,6 @@ "badgeTextContent": "the browsing time of current website", "locale": { "label": "Language {input}", - "default": "Follow browser", "changeConfirm": "The language has been changed successfully, please reload this page!", "reloadButton": "Reload" }, @@ -307,7 +299,6 @@ "darkMode": { "label": "Dark mode {input}", "options": { - "default": "Follow browser", "on": "Always on", "off": "Always off", "timed": "Timed on" @@ -316,7 +307,7 @@ "animationDuration": "The duration of the chart's initial animation {input}" }, "statistics": { - "title": "Statistics", + "title": "Tracking", "autoPauseTrack": "{input} Pause tracking if no activity detected {info} for {maxTime}", "noActivityInfo": "The mouse and keyboard are inactive and not in full screen mode", "countLocalFiles": "{input} Whether to track the time when the browser reads {localFileTime} {info}", @@ -325,9 +316,6 @@ "countTabGroup": "{input} Whether to track the time of tab groups {info}", "tabGroupInfo": "When you delete a tag group, the data will also be deleted.", "tabGroupsPermGrant": "This feature requires relevant permissions", - "collectSiteName": "{input} Whether to automatically collect {siteName} {siteNameUsage} when visiting the site homepage", - "siteName": "the site name", - "siteNameUsage": "The data is only stored locally and will be displayed instead of the URL to increase the recognition.Of course, you can also customize the name of each site.", "fileAccessDisabled": "Access to file URLs is currently not allowed. Please enable it on the manage page first", "fileAccessFirefox": "Sorry, this feature is not supported in Firefox", "weekStart": "The first day for each week {input}", @@ -418,10 +406,11 @@ "ja": { "yes": "はい", "no": "いいえ", + "followBrowser": "ブラウザと同じ", "popup": { "title": "ポップアップページ", "max": "最初の {input} 個のデータのみを表示し、残りのエントリは結合されます", - "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか" + "displaySiteName": "{input} ドメインの代わりにウェブサイト名を表示するかどうか" }, "appearance": { "title": "外観", @@ -434,7 +423,6 @@ "badgeTextContent": "現在のウェブサイトの閲覧時間", "locale": { "label": "言語設定 {input}", - "default": "ブラウザと同じ", "changeConfirm": "言語が正常に変更されました。このページをリロードしてください。", "reloadButton": "リロード" }, @@ -446,7 +434,6 @@ "darkMode": { "label": "ダークモード {input}", "options": { - "default": "ブラウザと同じ", "on": "常にオン", "off": "常にオフ", "timed": "時限スタート" @@ -461,9 +448,6 @@ "countLocalFiles": "{input} ブラウザで {localFileTime} {info} に費やされた時間をカウントするかどうか", "localFileTime": "ローカルファイルの読み取り", "localFilesInfo": "PDF、画像、txt、jsonを含む", - "collectSiteName": "{input} サイトのホームページにアクセスしたときに {siteName} {siteNameUsage} を自動的に収集するかどうか", - "siteName": "サイト名", - "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。", "fileAccessDisabled": "ファイル URL へのアクセスは現在許可されていません。まず管理ページで有効にしてください。", "fileAccessFirefox": "申し訳ありませんが、この機能はFirefoxではサポートされていません", "weekStart": "週の最初の日 {input}", @@ -553,10 +537,11 @@ "pt_PT": { "yes": "Sim", "no": "Não", + "followBrowser": "Usar do navegador", "popup": { "title": "Página Pop-up", "max": "Mostrar os primeiros {input} itens", - "displaySiteName": "{input} Mostrar {siteName} em vez do URL" + "displaySiteName": "{input} Se deve apresentar o nome do site em vez do domínio" }, "appearance": { "title": "Aparência", @@ -569,7 +554,6 @@ "badgeTextContent": "tempo de navegação no site atual", "locale": { "label": "Idioma {input}", - "default": "Usar do navegador", "changeConfirm": "Idioma alterado! Recarregue a página.", "reloadButton": "Recarregar" }, @@ -581,7 +565,6 @@ "darkMode": { "label": "Modo escuro {input}", "options": { - "default": "Usar do navegador", "on": "Sempre ativo", "off": "Sempre inativo", "timed": "Ativo por tempo" @@ -596,9 +579,6 @@ "countLocalFiles": "{input} Contar tempo a {localFileTime} {info}", "localFileTime": "ler ficheiros locais", "localFilesInfo": "Suporta PDF, imagens, txt e json.", - "collectSiteName": "{input} Recolher {siteName} {siteNameUsage}", - "siteName": "nome do site", - "siteNameUsage": "Dados armazenados localmente para substituir URLs. Pode personalizar nomes.", "fileAccessDisabled": "Acesso a URLs de ficheiro não permitido. Ative na página de gestão.", "fileAccessFirefox": "Não suportado no Firefox", "weekStart": "Primeiro dia da semana {input}", @@ -681,10 +661,11 @@ "uk": { "yes": "Так", "no": "Ні", + "followBrowser": "Як у браузері", "popup": { "title": "Вікно розширення", "max": "Кількість записів для показу: {input}", - "displaySiteName": "{input} Показувати {siteName} замість URL-адреси" + "displaySiteName": "{input} Чи відображати назву веб-сайту замість домену" }, "appearance": { "title": "Зовнішній вигляд", @@ -697,7 +678,6 @@ "badgeTextContent": "час перегляду поточного вебсайту", "locale": { "label": "Мова: {input}", - "default": "Як у браузері", "changeConfirm": "Мову успішно змінено. Перезавантажте сторінку!", "reloadButton": "Перезавантажити" }, @@ -709,7 +689,6 @@ "darkMode": { "label": "Темний режим: {input}", "options": { - "default": "Як у браузері", "on": "Увімкнено", "off": "Вимкнено", "timed": "За розкладом" @@ -724,9 +703,6 @@ "countLocalFiles": "{input} Враховувати час {localFileTime} {info} в браузері", "localFileTime": "перегляду локального файлу", "localFilesInfo": "Підтримуються файли PDF, зображення, текстові та формат json", - "collectSiteName": "{input} Автоматично записувати {siteName} {siteNameUsage} під час відвідування його головної сторінки", - "siteName": "назву сайту", - "siteNameUsage": "Дані зберігаються лише локально і будуть відображатися замість URL для зручності розпізнавання. Звісно, ви також можете налаштувати назву кожного сайту.", "fileAccessDisabled": "Доступ до URL-адрес файлу наразі не дозволено. Спершу ввімкніть на сторінці керування", "fileAccessFirefox": "На жаль, ця функція не підтримується у Firefox", "weekStart": "Перший день тижня: {input}", @@ -809,10 +785,11 @@ "es": { "yes": "Sí", "no": "No", + "followBrowser": "Igual que el navegador", "popup": { "title": "Página emergente", "max": "Mostrar los primeros {input} elementos de datos", - "displaySiteName": "{input} Mostrar {siteName} en lugar de la URL" + "displaySiteName": "{input} Si se debe mostrar el nombre del sitio web en lugar del dominio" }, "appearance": { "title": "Apariencia", @@ -825,7 +802,6 @@ "badgeTextContent": "el tiempo de navegación del sitio web actual", "locale": { "label": "Idioma {input}", - "default": "Igual que el navegador", "changeConfirm": "El idioma se ha cambiado con éxito, ¡por favor recarga esta página!", "reloadButton": "Recargar" }, @@ -837,7 +813,6 @@ "darkMode": { "label": "Modo oscuro {input}", "options": { - "default": "Igual que el navegador", "on": "Siempre encendido", "off": "Siempre apagado", "timed": "Cronometrado" @@ -855,9 +830,6 @@ "countTabGroup": "{input} Rastrear el tiempo de los grupos de pestañas {info}", "tabGroupInfo": "Al eliminar un grupo de pestañas, sus datos también se borrarán.", "tabGroupsPermGrant": "Esta función requiere permisos pertinentes", - "collectSiteName": "{input} Recopilar automáticamente {siteName} {siteNameUsage} al visitar la página principal del sitio", - "siteName": "el nombre del sitio", - "siteNameUsage": "Los datos solo se almacenan localmente y se mostrarán en lugar de la URL para aumentar el reconocimiento. Por supuesto, también puedes personalizar el nombre de cada sitio.", "fileAccessDisabled": "Actualmente no se permite el acceso a las URL de archivos. Habilítelo primero en la página de administración", "fileAccessFirefox": "Lo sentimos, esta función no es compatible con Firefox", "weekStart": "El primer día de cada semana {input}", @@ -947,10 +919,11 @@ "de": { "yes": "Ja", "no": "Nein", + "followBrowser": "Browser verfolgen", "popup": { "title": "Popup-Seite", "max": "Zeige die ersten {input} Datenelemente", - "displaySiteName": "{input} {siteName} statt URL anzeigen" + "displaySiteName": "{input} Ob der Websitename anstelle der Domäne angezeigt werden soll" }, "appearance": { "title": "Aussehen", @@ -963,7 +936,6 @@ "badgeTextContent": "die Besuchszeit der aktuellen Webseite", "locale": { "label": "Sprache {input}", - "default": "Browser verfolgen", "changeConfirm": "Die Sprache wurde erfolgreich geändert. Bitte lade diese Seite neu!", "reloadButton": "Neu laden" }, @@ -975,7 +947,6 @@ "darkMode": { "label": "Dunkler Modus {input}", "options": { - "default": "Browser verfolgen", "on": "Immer an", "off": "Immer aus", "timed": "Zeitgesteuert" @@ -989,9 +960,6 @@ "countLocalFiles": "{input} Zeit an {localFileTime} {info} im Browser zählen", "localFileTime": "eine lokale Datei lesen", "localFilesInfo": "Unterstützt Dateitypen, wie PDF, Bilder, .txt und .json", - "collectSiteName": "{input} Automatisch {siteName} {siteNameUsage} beim Besuch der Homepage loggen", - "siteName": "Der Name der Website", - "siteNameUsage": "Die Daten werden nur lokal gespeichert und anstelle der URL angezeigt, um die Erkennung zu erhöhen. Auf Wunsch lässt sich der Name jeder Seite anpassen.", "fileAccessDisabled": "Der Zugriff auf Datei-URLs ist derzeit nicht erlaubt. Bitte aktiviere ihn zuerst auf der Verwaltungsseite", "fileAccessFirefox": "Leider wird diese Funktion in Firefox nicht unterstützt", "weekStart": "Erster Tag der Woche {input}", @@ -1071,10 +1039,11 @@ "fr": { "yes": "Oui", "no": "Non", + "followBrowser": "Suivre le navigateur", "popup": { "title": "Page pop-up", "max": "Afficher les {input} premiers éléments de données", - "displaySiteName": "{input} S'il faut afficher {siteName} au lieu de l'URL" + "displaySiteName": "{input} S'il faut afficher le nom du site Web au lieu du domaine" }, "appearance": { "title": "Apparence", @@ -1087,7 +1056,6 @@ "badgeTextContent": "le temps de navigation du site actuel", "locale": { "label": "Langue {input}", - "default": "Suivre le navigateur", "changeConfirm": "La langue a été modifiée avec succès, veuillez recharger cette page !", "reloadButton": "Redémarrer" }, @@ -1099,7 +1067,6 @@ "darkMode": { "label": "Mode sombre {input}", "options": { - "default": "Suivre le navigateur", "on": "Toujours On", "off": "Toujours éteint", "timed": "Horaire" @@ -1117,9 +1084,6 @@ "countTabGroup": "{input} Suivre le temps des groupes d'onglets {info}", "tabGroupInfo": "Lorsque vous supprimez un groupe d'onglets, les données seront également supprimées.", "tabGroupsPermGrant": "Cette fonctionnalité nécessite des autorisations pertinentes", - "collectSiteName": "{input} S'il faut collecter automatiquement {siteName} {siteNameUsage} lors de la visite de la page d'accueil du site", - "siteName": "le nom du site", - "siteNameUsage": "Les données sont uniquement stockées localement et seront affichées à la place de l'URL pour augmenter la reconnaissance. Bien entendu, vous pouvez également personnaliser le nom de chaque site.", "fileAccessDisabled": "L'accès aux URL des fichiers n'est actuellement pas autorisé. Veuillez d'abord l'activer dans la page de gestion.", "fileAccessFirefox": "Désolé, cette fonctionnalité n'est pas prise en charge dans Firefox", "weekStart": "Le premier jour de chaque semaine {input}", @@ -1212,7 +1176,7 @@ "popup": { "title": "Всплывающее окно", "max": "Показать первые {input} элементов данных", - "displaySiteName": "{input} Отображать ли {siteName} вместо URL" + "displaySiteName": "{input} Отображать ли имя веб-сайта вместо домена" }, "appearance": { "title": "Появление", @@ -1256,10 +1220,11 @@ "ar": { "yes": "نعم", "no": "لا", + "followBrowser": "نفس وضع المتصفح", "popup": { "title": "صفحة منبثقة", "max": "عرض أول {input} عنصر من البيانات", - "displaySiteName": "{input} هل يتم عرض {siteName} بدلاً من الرابط" + "displaySiteName": "{input} هل سيتم عرض اسم الموقع بدلاً من النطاق" }, "appearance": { "title": "المظهر", @@ -1272,7 +1237,6 @@ "badgeTextContent": "وقت التصفح للموقع الحالي", "locale": { "label": "اللغة {input}", - "default": "لغة المتصفح", "changeConfirm": "تم تغيير اللغة بنجاح، يرجى إعادة تحميل هذه الصفحة!", "reloadButton": "إعادة التحميل" }, @@ -1284,7 +1248,6 @@ "darkMode": { "label": "الوضع الداكن {input}", "options": { - "default": "نفس وضع المتصفح", "on": "تفعيل دائم", "off": "إيقاف دائم", "timed": "تفعيل مؤقت" @@ -1302,9 +1265,6 @@ "countTabGroup": "{input} هل يتم تتبع وقت مجموعات علامات التبويب {info}", "tabGroupInfo": "عند حذف مجموعة علامات تبويب، سيتم حذف البيانات أيضًا.", "tabGroupsPermGrant": "هذه الميزة تتطلب أذونات ذات صلة", - "collectSiteName": "{input} هل يتم أخذ {siteName} {siteNameUsage} تلقائيًا عند زيارة الصفحة الرئيسية", - "siteName": "اسم الموقع الإلكتروني", - "siteNameUsage": "يتم تخزين البيانات محليًا فقط وسيتم عرضها بدلاً من الرابط لزيادة الوضوح. يمكنك أيضًا تخصيص اسم كل موقع.", "fileAccessDisabled": "الوصول إلى عناوين الملفات غير مسموح به حاليًا. يرجى تفعيل الخاصية من صفحة الإدارة أولاً", "fileAccessFirefox": "عذراً، هذه الميزة غير مدعومة في فايرفوكس", "weekStart": "اليوم الأول لكل أسبوع {input}", diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 55a0d55e4..070928947 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -9,6 +9,7 @@ import resource from './option-resource.json' export type OptionMessage = { yes: string no: string + followBrowser: string popup: { title: string max: string @@ -27,7 +28,6 @@ export type OptionMessage = { badgeTextContent: string locale: { label: string - default: string changeConfirm: string reloadButton: string } @@ -38,7 +38,7 @@ export type OptionMessage = { }, darkMode: { label: string - options: Record + options: Omit, 'default'> } animationDuration: string } @@ -52,9 +52,6 @@ export type OptionMessage = { countTabGroup: string tabGroupInfo: string tabGroupsPermGrant: string - collectSiteName: string - siteNameUsage: string - siteName: string fileAccessDisabled: string fileAccessFirefox: string weekStart: string diff --git a/src/pages/app/components/Option/components/AccessibilityOption.tsx b/src/pages/app/components/Option/components/AccessibilityOption.tsx index fc834e01b..9fbd03495 100644 --- a/src/pages/app/components/Option/components/AccessibilityOption.tsx +++ b/src/pages/app/components/Option/components/AccessibilityOption.tsx @@ -1,4 +1,3 @@ -import { t } from "@app/locale" import { defaultAccessibility } from "@util/constant/option" import { ElSwitch } from "element-plus" import { defineComponent } from "vue" @@ -15,19 +14,11 @@ const _default = defineComponent((_, ctx) => { ctx.expose({ reset: () => copy(option, defaultAccessibility()) } satisfies OptionInstance) - return () => <> - msg.option.accessibility.chartDecal} - defaultValue={t(msg => msg.option.no)} - hideDivider - v-slots={{ - default: () => option.chartDecal = val as boolean} - /> - }} - /> - + return () => ( + msg.option.accessibility.chartDecal} defaultValue={false}> + option.chartDecal = val as boolean} /> + + ) }) export default _default \ No newline at end of file diff --git a/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx b/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx index c3eb49b9f..3b2a3bdf6 100644 --- a/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx +++ b/src/pages/app/components/Option/components/AppearanceOption/DarkModeInput.tsx @@ -48,7 +48,10 @@ const _default = defineComponent(props => { onChange={val => props.onChange?.(val as timer.option.DarkMode, [props.startSecond, props.endSecond])} > { - ALL_MODES.map(value => msg.option.appearance.darkMode.options[value])} />) + ALL_MODES.map(value => value === 'default' ? msg.option.followBrowser : msg.option.appearance.darkMode.options[value])} + />) } {props.modelValue === "timed" && <> diff --git a/src/pages/app/components/Option/components/AppearanceOption/index.tsx b/src/pages/app/components/Option/components/AppearanceOption/index.tsx index 2b8ab1198..e55d6405d 100644 --- a/src/pages/app/components/Option/components/AppearanceOption/index.tsx +++ b/src/pages/app/components/Option/components/AppearanceOption/index.tsx @@ -5,18 +5,19 @@ * https://opensource.org/licenses/MIT */ -import { t, tWith } from "@app/locale" +import { type I18nKey, t, tWith } from "@app/locale" import { ALL_LOCALES, localeSameAsBrowser } from "@i18n" import localeMessages from "@i18n/message/common/locale" import optionService from "@service/option-service" import { IS_ANDROID } from "@util/constant/environment" import { defaultAppearance } from "@util/constant/option" import { toggle } from "@util/dark-mode" -import { ElColorPicker, ElMessageBox, ElOption, ElSelect, ElSlider, ElSwitch, ElTag } from "element-plus" +import { ElColorPicker, ElMessageBox, ElOption, ElSelect, ElSlider, ElSwitch, ElTag, type TagProps } from "element-plus" import { computed, defineComponent, type StyleValue } from "vue" import { type OptionInstance } from "../../common" import { useOption } from "../../useOption" import OptionItem from "../OptionItem" +import OptionLines from '../OptionLines' import OptionTag from "../OptionTag" import DarkModeInput from "./DarkModeInput" @@ -38,6 +39,7 @@ function copy(target: timer.option.AppearanceOption, source: timer.option.Appear } const DEFAULT_ANIMA_DURATION = defaultAppearance().chartAnimationDuration +const FOLLOW_BROWSER: I18nKey = msg => msg.option.followBrowser const _default = defineComponent((_props, ctx) => { const { option } = useOption({ @@ -64,7 +66,7 @@ const _default = defineComponent((_props, ctx) => { closeOnClickModal: false }).then(() => { location.reload?.() }).catch(() => {/* do nothing */ }) } - const animaDurationTagType = computed<'info' | 'primary' | 'warning'>(() => { + const animaDurationTagType = computed(() => { const val = option.chartAnimationDuration if (!val) return 'info' if (val > DEFAULT_ANIMA_DURATION) return 'warning' @@ -72,12 +74,8 @@ const _default = defineComponent((_props, ctx) => { }) return () => ( -
    - msg.option.appearance.darkMode.label} - defaultValue={t(msg => msg.option.appearance.darkMode.options.default)} - hideDivider - > + + msg.option.appearance.darkMode.label} defaultValue={FOLLOW_BROWSER}> { }} /> - msg.option.appearance.locale.label} - defaultValue={t(msg => msg.option.appearance.locale.default)} - > + msg.option.appearance.locale.label} defaultValue={FOLLOW_BROWSER}> { > {allLocaleOptions.map(locale => msg.option.appearance.locale.default) : localeMessages[locale].name} + label={locale === "default" ? t(FOLLOW_BROWSER) : localeMessages[locale].name} />)} @@ -179,7 +174,7 @@ const _default = defineComponent((_props, ctx) => { {option.chartAnimationDuration}ms -
    + ) }) diff --git a/src/pages/app/components/Option/components/BackupOption/index.tsx b/src/pages/app/components/Option/components/BackupOption/index.tsx index eda644108..01049c684 100644 --- a/src/pages/app/components/Option/components/BackupOption/index.tsx +++ b/src/pages/app/components/Option/components/BackupOption/index.tsx @@ -13,6 +13,7 @@ import { ElInput, ElOption, ElSelect } from "element-plus" import { computed, defineComponent } from "vue" import { type OptionInstance } from "../../common" import OptionItem from "../OptionItem" +import OptionLines from '../OptionLines' import OptionTooltip from "../OptionTooltip" import AutoInput from "./AutoInput" import Footer from "./Footer" @@ -45,8 +46,8 @@ const _default = defineComponent((_, ctx) => { ctx.expose({ reset } satisfies OptionInstance) - return () => <> - msg.option.backup.type} defaultValue={TYPE_NAMES['none']} hideDivider> + return () => + msg.option.backup.type} defaultValue={TYPE_NAMES['none']}> { onInput={val => setExtField('endpoint', val)} /> - "Vault Name {input}"}> + "Vault Name {input}"}> { onInput={val => setExtField('bucket', val)} /> - msg.option.backup.label.path} required> + msg.option.backup.label.path} required> { onInput={val => setExtField('dirPath', val)} /> - "Authorization {input}"}> + "Authorization {input}"} required> { onInput={val => setExtField('endpoint', val)} /> - msg.option.backup.label.path} required> + msg.option.backup.label.path} required> { onInput={val => setExtField('dirPath', val)} /> - msg.option.backup.label.account} required> + msg.option.backup.label.account} required> { onInput={val => account.value = val?.trim?.()} /> - msg.option.backup.label.password} required> + msg.option.backup.label.password} required> { /> {isNotNone.value &&