diff --git a/src/app/components/About/Description.tsx b/src/app/components/About/Description.tsx index 9e71c2ce..723717a9 100644 --- a/src/app/components/About/Description.tsx +++ b/src/app/components/About/Description.tsx @@ -1,4 +1,4 @@ -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import { HOMEPAGE, CHROME_HOMEPAGE, EDGE_HOMEPAGE, FIREFOX_HOMEPAGE, FEEDBACK_QUESTIONNAIRE, GITHUB_ISSUE_ADD, REVIEW_PAGE, @@ -9,7 +9,6 @@ import { ElCard, ElDescriptions, ElDescriptionsItem, ElDivider, ElSpace, ElText import { defineComponent, StyleValue } from "vue" import InstallationLink from "./InstallationLink" import packageInfo, { AUTHOR_EMAIL } from "@src/package" -import { locale } from "@i18n" import "./description.sass" import metaService from "@service/meta-service" import DescLink from "./DescLink" diff --git a/src/app/components/Analysis/components/Summary/Calendar/Wrapper.ts b/src/app/components/Analysis/components/Summary/Calendar/Wrapper.ts index 62224a90..8e34f9d4 100644 --- a/src/app/components/Analysis/components/Summary/Calendar/Wrapper.ts +++ b/src/app/components/Analysis/components/Summary/Calendar/Wrapper.ts @@ -14,13 +14,12 @@ import { EffectScatterSeriesOption } from "echarts" import { formatTime, getAllDatesBetween, getWeeksAgo, parseTime } from "@util/time" -import { locale } from "@i18n" import { EffectScatterChart } from "echarts/charts" import { SVGRenderer } from "echarts/renderers" import { use } from "echarts/core" import { GridComponent, TitleComponent, TooltipComponent, VisualMapComponent } from "echarts/components" import { groupBy, rotate } from "@util/array" -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import { getRegularTextColor, getSecondaryTextColor } from "@util/style" import { periodFormatter } from "@app/util/time" diff --git a/src/app/components/Dashboard/components/Calendar/Wrapper.ts b/src/app/components/Dashboard/components/Calendar/Wrapper.ts index e0586d7e..4666489a 100644 --- a/src/app/components/Dashboard/components/Calendar/Wrapper.ts +++ b/src/app/components/Dashboard/components/Calendar/Wrapper.ts @@ -21,10 +21,9 @@ use([ ]) import { EchartsWrapper } from "@hooks" -import { formatPeriodCommon, getAllDatesBetween, MILL_PER_HOUR } from "@util/time" -import { locale } from "@i18n" +import { formatPeriodCommon, getAllDatesBetween, MILL_PER_HOUR, MILL_PER_MINUTE } from "@util/time" import { groupBy, rotate, sum } from "@util/array" -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import { getPrimaryTextColor } from "@util/style" import { BASE_TITLE_OPTION } from "../../common" import { getAppPageUrl } from "@util/constant/url" @@ -109,8 +108,8 @@ type Piece = { color?: string } -const minOf = (min: number) => min * 60 * 1000 -const hourOf = (hour: number) => hour * 60 * 60 * 1000 +const minOf = (min: number) => min * MILL_PER_MINUTE +const hourOf = (hour: number) => hour * MILL_PER_HOUR const ALL_PIECES: Piece[] = [ { min: 1, max: minOf(10), label: "<10m" }, diff --git a/src/app/components/Dashboard/components/Calendar/index.tsx b/src/app/components/Dashboard/components/Calendar/index.tsx index 53ad7e13..fac17ae4 100644 --- a/src/app/components/Dashboard/components/Calendar/index.tsx +++ b/src/app/components/Dashboard/components/Calendar/index.tsx @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ import { useEcharts } from "@hooks" -import { locale } from "@i18n" +import { locale } from "@app/locale" import statService from "@service/stat-service" import { getWeeksAgo } from "@util/time" import { defineComponent } from "vue" diff --git a/src/app/components/DataManage/ClearPanel/index.tsx b/src/app/components/DataManage/ClearPanel/index.tsx index 5615da8f..69148e67 100644 --- a/src/app/components/DataManage/ClearPanel/index.tsx +++ b/src/app/components/DataManage/ClearPanel/index.tsx @@ -10,7 +10,7 @@ import { defineComponent } from "vue" import { t } from "@app/locale" import { alertProps } from "../common" import ClearFilter from "./ClearFilter" -import { MILL_PER_DAY } from "@util/time" +import { MILL_PER_DAY, MILL_PER_SECOND } from "@util/time" import statService, { StatQueryParam } from "@service/stat-service" type FilterOption = { @@ -54,7 +54,7 @@ function assertQueryParam(range: Vector<2>, mustInteger?: boolean): boolean { } const str2Num = (str: string, defaultVal?: number) => (str && str !== '') ? parseInt(str) : defaultVal -const seconds2Milliseconds = (a: number) => a * 1000 +const seconds2Milliseconds = (a: number) => a * MILL_PER_SECOND function checkParam(option: FilterOption): StatQueryParam | undefined { const { focus = [null, null], time = [null, null] } = option || {} diff --git a/src/app/components/DataManage/Migration/ImportOtherButton/processor.ts b/src/app/components/DataManage/Migration/ImportOtherButton/processor.ts index 0a848c5b..cef1b0a1 100644 --- a/src/app/components/DataManage/Migration/ImportOtherButton/processor.ts +++ b/src/app/components/DataManage/Migration/ImportOtherButton/processor.ts @@ -8,7 +8,7 @@ import { fillExist } from "@service/components/import-processor" import { AUTHOR_EMAIL } from "@src/package" import { extractHostname, isBrowserUrl } from "@util/pattern" -import { formatTime } from "@util/time" +import { formatTimeYMD, MILL_PER_SECOND } from "@util/time" export type OtherExtension = | "webtime_tracker" @@ -52,7 +52,7 @@ async function parseWebActivityTimeTracker(file: File): Promise { .map(([host, date, seconds]) => ({ host, date, - focus: seconds * 1000 + focus: seconds * MILL_PER_SECOND } as timer.imported.Row)) return rows } else if (isCsvFile(file)) { @@ -103,7 +103,7 @@ async function parseWebtimeTracker(file: File): Promise { for (let i = 1; i < colHeaders?.length; i++) { const seconds = Number.parseInt(cells[i]) const date = cvtWebtimeTrackerDate(colHeaders[i]) - seconds && date && rows.push({ host, date, focus: seconds * 1000 }) + seconds && date && rows.push({ host, date, focus: seconds * MILL_PER_SECOND }) } }) return rows @@ -120,7 +120,7 @@ function parseHistoryTrendsUnlimitedLine(line: string, data: { [dateAndHost: str // Backup data let date: string; try { - date = formatTime(parseFloat(tsMaybe.substring(1)), "{y}{m}{d}") + date = formatTimeYMD(parseFloat(tsMaybe.substring(1))) } catch { console.error("Invalid line: " + line) return; diff --git a/src/app/components/Habit/components/Site/Distribution/Wrapper.ts b/src/app/components/Habit/components/Site/Distribution/Wrapper.ts index 0c9b9be1..bad9afb4 100644 --- a/src/app/components/Habit/components/Site/Distribution/Wrapper.ts +++ b/src/app/components/Habit/components/Site/Distribution/Wrapper.ts @@ -126,7 +126,7 @@ const baseLegendTitle = (): TitleComponentOption => ({ }) function generateOption(bizOption: BizOption): EcOption { - let { rows, dateRange } = bizOption || {} + let { rows = [], dateRange } = bizOption || {} const [averageLen, _, exclusiveDate] = computeAverageLen(dateRange) if (exclusiveDate) { rows = rows.filter(r => r.date !== exclusiveDate) diff --git a/src/app/components/Habit/components/Site/common.ts b/src/app/components/Habit/components/Site/common.ts index 4149b06d..1a7c38f2 100644 --- a/src/app/components/Habit/components/Site/common.ts +++ b/src/app/components/Habit/components/Site/common.ts @@ -8,9 +8,7 @@ import type { TitleComponentOption } from "echarts/components" import { getRegularTextColor } from "@util/style" -import { generateSiteLabel } from "@util/site" -import { periodFormatter } from "@app/util/time" -import { formatTime, getDayLength, isSameDay } from "@util/time" +import { formatTimeYMD, getDayLength, isSameDay } from "@util/time" export const generateTitleOption = (text: string): TitleComponentOption => { const secondaryTextColor = getRegularTextColor() @@ -42,7 +40,7 @@ export const computeAverageLen = (dateRange: [Date, Date] = [null, null]): [numb const dateDiff = getDayLength(start, end) const endIsTody = isSameDay(end, new Date()) if (endIsTody) { - return [dateDiff - 1, true, formatTime(end, "{y}{m}{d}")] + return [dateDiff - 1, true, formatTimeYMD(end)] } else { return [dateDiff, false, null] } diff --git a/src/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx b/src/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx index 21f0485e..5960141a 100644 --- a/src/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx +++ b/src/app/components/Limit/LimitModify/Sop/Step3/TimeInput.tsx @@ -65,6 +65,7 @@ function computeLimitInfo2Second(hour: number, minute: number, second: number): const _default = defineComponent({ props: { modelValue: Number, + hourMax: Number }, emits: { change: (_val: number) => true @@ -83,12 +84,13 @@ const _default = defineComponent({ const limitTime = computed(() => computeLimitInfo2Second(hour.value, minute.value, second.value)) watch(limitTime, () => ctx.emit('change', limitTime.value)) - return () =>
- - - -
- + return () => ( +
+ + + +
+ ) } }) diff --git a/src/app/components/Limit/LimitModify/Sop/Step3/index.tsx b/src/app/components/Limit/LimitModify/Sop/Step3/index.tsx index 5d19fdb7..2acec7d0 100644 --- a/src/app/components/Limit/LimitModify/Sop/Step3/index.tsx +++ b/src/app/components/Limit/LimitModify/Sop/Step3/index.tsx @@ -14,24 +14,38 @@ import { useShadow } from "@hooks" import { StepFromInstance } from "../common" import "./style.sass" +type Value = Pick + +const MAX_HOUR_WEEKLY = 7 * 24 - 1 + const _default = defineComponent({ props: { time: Number, visitTime: Number, + weekly: Number, periods: Array as PropType, }, emits: { - change: (_time: number, _visitTime: number, _periods: Vector<2>[]) => true, + change: (_val: Value) => true, }, setup(props, ctx) { const [time, setTime] = useShadow(() => props.time) + const [weekly, setWeekly] = useShadow(() => props.weekly) const [visitTime, setVisitTime] = useShadow(() => props.visitTime) const [periods, setPeriods] = useShadow(() => props.periods, []) - watch([time, visitTime, periods], () => ctx.emit("change", time.value, visitTime.value, periods.value)) + watch([time, visitTime, periods, weekly], () => { + const val: Value = { + time: time.value, + visitTime: visitTime.value, + weekly: weekly.value, + periods: periods.value, + } + ctx.emit("change", val) + }) const validate = () => { - if (!time.value && !visitTime.value && !periods.value?.length) { + if (!time.value && !visitTime.value && !periods.value?.length && !weekly.value) { ElMessage.error(t(msg => msg.limit.message.noRule)) return false } @@ -44,6 +58,13 @@ const _default = defineComponent({ msg.limit.item.time)}> + msg.limit.item.weekly)}> + + msg.limit.item.visitTime)}> diff --git a/src/app/components/Limit/LimitModify/Sop/index.tsx b/src/app/components/Limit/LimitModify/Sop/index.tsx index d5c0cfa7..d0cb73d2 100644 --- a/src/app/components/Limit/LimitModify/Sop/index.tsx +++ b/src/app/components/Limit/LimitModify/Sop/index.tsx @@ -28,6 +28,7 @@ export type SopInstance = { const createInitial = (): Required> => ({ name: null, time: 3600, + weekly: null, cond: [], visitTime: null, periods: null, @@ -102,12 +103,14 @@ const _default = defineComponent({ v-show={step.value === 2} ref={stepInstances[2]} time={data.time} + weekly={data.weekly} visitTime={data.visitTime} periods={data.periods} - onChange={(time, visitTime, periods) => { + onChange={({ time, visitTime, periods, weekly }) => { data.time = time data.visitTime = visitTime data.periods = periods + data.weekly = weekly }} /> diff --git a/src/app/components/Limit/LimitModify/index.tsx b/src/app/components/Limit/LimitModify/index.tsx index 8717db8f..315aec9a 100644 --- a/src/app/components/Limit/LimitModify/index.tsx +++ b/src/app/components/Limit/LimitModify/index.tsx @@ -32,10 +32,10 @@ const _default = defineComponent({ let modifyingItem: timer.limit.Rule = undefined const handleSave = async (rule: timer.limit.Rule) => { - const { cond, enabled, name, time, visitTime, periods, weekdays } = rule + const { cond, enabled, name, time, weekly, visitTime, periods, weekdays } = rule const toSave: timer.limit.Rule = { ...modifyingItem || {}, - cond, enabled, name, time, visitTime, weekdays, + cond, enabled, name, time, weekly, visitTime, weekdays, // Object to array periods: periods?.map(i => [i?.[0], i?.[1]]), } diff --git a/src/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx b/src/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx index c5484436..6c280b32 100644 --- a/src/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx +++ b/src/app/components/Limit/LimitTable/column/LimitOperationColumn.tsx @@ -8,9 +8,8 @@ import { Delete, Edit } from "@element-plus/icons-vue" import { ElButton, ElMessageBox, ElTableColumn } from "element-plus" import { defineComponent } from "vue" -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import optionService from "@service/option-service" -import { locale } from "@i18n" import { ElTableRowScope } from "@src/element-ui/table" import { judgeVerificationRequired, processVerification } from "@app/util/limit" @@ -34,8 +33,7 @@ async function handleDelete(row: timer.limit.Item, callback: () => void) { async function handleModify(row: timer.limit.Item, callback: () => void) { let promise: Promise = undefined if (await judgeVerificationRequired(row)) { - const option = - (await optionService.getAllOption()) as timer.option.DailyLimitOption + const option = (await optionService.getAllOption()) as timer.option.DailyLimitOption promise = processVerification(option) promise ? promise.then(callback).catch(() => { }) : callback() } else { diff --git a/src/app/components/Limit/LimitTable/index.tsx b/src/app/components/Limit/LimitTable/index.tsx index 8f7988da..0cce3f8b 100644 --- a/src/app/components/Limit/LimitTable/index.tsx +++ b/src/app/components/Limit/LimitTable/index.tsx @@ -4,7 +4,7 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElTable, ElTableColumn, ElTag } from "element-plus" +import { ElIcon, ElTable, ElTableColumn, ElTag, ElTooltip } from "element-plus" import { defineComponent, PropType } from "vue" import { t } from "@app/locale" import { formatPeriod, formatPeriodCommon, MILL_PER_SECOND } from "@util/time" @@ -13,15 +13,21 @@ import { period2Str } from "@util/limit" import LimitDelayColumn from "./column/LimitDelayColumn" import LimitEnabledColumn from "./column/LimitEnabledColumn" import LimitOperationColumn from "./column/LimitOperationColumn" +import { InfoFilled } from "@element-plus/icons-vue" +import ColumnHeader from "@app/components/common/ColumnHeader" +import weekHelper from "@service/components/week-helper" +import { useRequest } from "@hooks" const ALL_WEEKDAYS = t(msg => msg.calendar.weekDays)?.split('|') const renderWeekdays = (weekdays: number[]) => { const len = weekdays?.length if (!len || len === 7) { - return - {t(msg => msg.calendar.range.everyday)} - + return ( + + {t(msg => msg.calendar.range.everyday)} + + ) } return ( @@ -38,41 +44,76 @@ const timeMsg = { } const renderDetail = (row: timer.limit.Item) => { - const { time, visitTime, periods } = row - return
- {!!time &&
- - {t(msg => msg.limit.item.time)}: {formatPeriod(time * MILL_PER_SECOND, timeMsg)} - -
} - {!!visitTime &&
- - {t(msg => msg.limit.item.visitTime)}: {formatPeriod(visitTime * MILL_PER_SECOND, timeMsg)} - -
} - {!!periods?.length &&
- {t(msg => msg.limit.item.period)} -
} - {!!periods?.length &&
- {periods.map(p => {period2Str(p)})} -
} -
+ const { time, weekly, visitTime, periods } = row + return ( +
+ {!!time && ( +
+ + {t(msg => msg.limit.item.time)}: {formatPeriod(time * MILL_PER_SECOND, timeMsg)} + +
+ )} + {!!weekly && ( +
+ + {t(msg => msg.limit.item.weekly)}: {formatPeriod(weekly * MILL_PER_SECOND, timeMsg)} + +
+ )} + {!!visitTime && ( +
+ + {t(msg => msg.limit.item.visitTime)}: {formatPeriod(visitTime * MILL_PER_SECOND, timeMsg)} + +
+ )} + {!!periods?.length && <> +
+ {t(msg => msg.limit.item.period)} +
+
+ {periods.map(p => {period2Str(p)})} +
+ } +
+ ) } const renderToday = (row: timer.limit.Item) => { const { waste, delayCount, allowDelay } = row - return
-
- {formatPeriodCommon(waste)} + return ( +
+
+ {formatPeriodCommon(waste)} +
+ {(!!allowDelay || !!delayCount) && ( +
+ + {t(msg => msg.limit.item.delayCount)}: {delayCount ?? 0} + +
+ )}
- {(!!allowDelay || !!delayCount) && ( + ) +} + +const renderWeekly = (row: timer.limit.Item) => { + const { weeklyWaste, weeklyDelayCount, allowDelay } = row + return ( +
- - {t(msg => msg.limit.item.delayCount)}: {delayCount ?? 0} - + {formatPeriodCommon(weeklyWaste)}
- )} -
+ {(!!allowDelay || !!weeklyDelayCount) && ( +
+ + {t(msg => msg.limit.item.delayCount)}: {weeklyDelayCount ?? 0} + +
+ )} +
+ ) } const _default = defineComponent({ @@ -86,18 +127,23 @@ const _default = defineComponent({ modify: (_row: timer.limit.Item) => true, }, setup(props, ctx) { + const { data: weekStartName } = useRequest(async () => { + const offset = await weekHelper.getRealWeekStart() + const name = t(msg => msg.calendar.weekDays)?.split('|')?.[offset] + return name || 'NaN' + }) return () => ( msg.limit.item.name)} - minWidth={160} + minWidth={140} align="center" formatter={({ name }: timer.limit.Item) => name || '-'} fixed /> msg.limit.item.condition)} - minWidth={320} + minWidth={200} align="center" formatter={({ cond }: timer.limit.Item) => <>{cond?.map?.(c => {c}) || ''}} /> @@ -119,10 +165,22 @@ const _default = defineComponent({ label={t(msg => msg.limit.item.waste)} minWidth={110} align="center" - // formatter={({ waste }: timer.limit.Item) => formatPeriodCommon(waste)} > {({ row }: ElTableRowScope) => renderToday(row)} + ( + msg.limit.item.wasteWeekly)} + tooltipContent={t(msg => msg.limit.item.weekStartInfo, { weekStart: weekStartName.value })} + /> + ), + default: ({ row }: ElTableRowScope) => renderWeekly(row), + }} + /> ctx.emit("delayChange", row)} /> ctx.emit("enabledChange", row)} /> = { const tStyle = (key: I18nKey) => t_(STYLES, { key }) -const weekStartOptionPairs: [[timer.option.WeekStartOption, string]] = [ - ['default', t(msg => msg.option.popup.weekStartAsNormal)] -] -const allWeekDays = t(msg => msg.calendar.weekDays) - .split('|') - .map((weekDay, idx) => [idx + 1, weekDay] as [timer.option.WeekStartOption, string]) -rotate(allWeekDays, locale === 'zh_CN' ? 0 : 1, true) -allWeekDays.forEach(weekDayInfo => weekStartOptionPairs.push(weekDayInfo)) - const defaultPopOptions = defaultPopup() const defaultTypeLabel = t(msg => msg.item[defaultPopOptions.defaultType]) const defaultDurationLabel = t(msg => msg.duration[defaultPopOptions.defaultDuration]) @@ -68,7 +57,6 @@ function copy(target: timer.option.PopupOption, source: timer.option.PopupOption target.defaultType = source.defaultType target.displaySiteName = source.displaySiteName target.popupMax = source.popupMax - target.weekStart = source.weekStart } const _default = defineComponent((_props, ctx) => { @@ -118,15 +106,6 @@ const _default = defineComponent((_props, ctx) => { ) }} /> - msg.option.popup.weekStart} defaultValue={t(msg => msg.option.popup.weekStartAsNormal)}> - option.weekStart = val} - > - {weekStartOptionPairs.map(([val, label]) => )} - - msg.option.popup.max} defaultValue={defaultPopOptions.popupMax} diff --git a/src/app/components/Option/components/StatisticsOption.tsx b/src/app/components/Option/components/StatisticsOption.tsx index f18af5ae..44b2c205 100644 --- a/src/app/components/Option/components/StatisticsOption.tsx +++ b/src/app/components/Option/components/StatisticsOption.tsx @@ -4,19 +4,30 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import { ElSwitch, ElTooltip } from "element-plus" +import { ElOption, ElSelect, ElSwitch, ElTooltip } from "element-plus" import optionService from "@service/option-service" import { defaultStatistics } from "@util/constant/option" import { defineComponent, reactive, unref, watch } from "vue" -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import { OptionInstance, OptionItem, OptionTag, OptionTooltip } from "../common" import { useRequest } from "@hooks" import { isAllowedFileSchemeAccess } from "@api/chrome/runtime" import { IS_FIREFOX } from "@util/constant/environment" +import { rotate } from "@util/array" + +const weekStartOptionPairs: [[timer.option.WeekStartOption, string]] = [ + ['default', t(msg => msg.option.statistics.weekStartAsNormal)] +] +const allWeekDays = t(msg => msg.calendar.weekDays) + .split('|') + .map((weekDay, idx) => [idx + 1, weekDay] as [timer.option.WeekStartOption, string]) +rotate(allWeekDays, locale === 'zh_CN' ? 0 : 1, true) +allWeekDays.forEach(weekDayInfo => weekStartOptionPairs.push(weekDayInfo)) function copy(target: timer.option.StatisticsOption, source: timer.option.StatisticsOption) { target.collectSiteName = source.collectSiteName target.countLocalFiles = source.countLocalFiles + target.weekStart = source.weekStart } const _default = defineComponent((_props, ctx) => { @@ -60,6 +71,15 @@ const _default = defineComponent((_props, ctx) => { />, }} /> + msg.option.statistics.weekStart} defaultValue={t(msg => msg.option.statistics.weekStartAsNormal)}> + option.weekStart = val} + > + {weekStartOptionPairs.map(([val, label]) => )} + + }) diff --git a/src/app/components/Report/ReportTable/columns/OperationColumn.tsx b/src/app/components/Report/ReportTable/columns/OperationColumn.tsx index 4a4a52b0..37fa0a6b 100644 --- a/src/app/components/Report/ReportTable/columns/OperationColumn.tsx +++ b/src/app/components/Report/ReportTable/columns/OperationColumn.tsx @@ -8,13 +8,12 @@ import { computed, defineComponent, onMounted, ref } from "vue" import { ElButton, ElMessage, ElTableColumn } from "element-plus" import StatDatabase from "@db/stat-database" import whitelistService from "@service/whitelist-service" -import { t } from "@app/locale" +import { t, locale } from "@app/locale" import { LocationQueryRaw, useRouter } from "vue-router" import { ANALYSIS_ROUTE } from "@app/router/constants" import { Open, Plus, Stopwatch } from "@element-plus/icons-vue" import PopupConfirmButton from "@app/components/common/PopupConfirmButton" import OperationDeleteButton from "./OperationDeleteButton" -import { locale } from "@i18n" import { useReportFilter } from "../../context" import { ElTableRowScope } from "@src/element-ui/table" diff --git a/src/app/components/Report/file-export.ts b/src/app/components/Report/file-export.ts index 2ba67ed3..48eda25d 100644 --- a/src/app/components/Report/file-export.ts +++ b/src/app/components/Report/file-export.ts @@ -6,7 +6,7 @@ */ import { t } from "@app/locale" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" import { periodFormatter } from "@app/util/time" import { exportCsv as exportCsv_, @@ -30,8 +30,8 @@ function computeFileName(filterParam: ReportFilterOption): string { if (dateRange && dateRange.length === 2) { const start = dateRange[0] const end = dateRange[1] - baseName += '_' + formatTime(start, '{y}{m}{d}') - baseName += '_' + formatTime(end, '{y}{m}{d}') + baseName += '_' + formatTimeYMD(start) + baseName += '_' + formatTimeYMD(end) } mergeDate && (baseName += '_' + t(msg => msg.report.mergeDate)) mergeHost && (baseName += '_' + t(msg => msg.report.mergeDomain)) diff --git a/src/app/locale.ts b/src/app/locale.ts index 0d2c5ab1..c61281c3 100644 --- a/src/app/locale.ts +++ b/src/app/locale.ts @@ -5,11 +5,13 @@ * https://opensource.org/licenses/MIT */ -import { I18nResultItem, I18nKey as _I18nKey, t as _t } from "@i18n" +import { I18nResultItem, I18nKey as _I18nKey, t as _t, locale as _locale } from "@i18n" import { tN as _tN } from "@i18n" import messages, { AppMessage } from "@i18n/message/app" import type { VNode } from "vue" +export const locale = _locale + export type I18nKey = _I18nKey export function t(key: I18nKey, param?: any) { diff --git a/src/app/util/limit.tsx b/src/app/util/limit.tsx index 4f9f0aca..8acde78f 100644 --- a/src/app/util/limit.tsx +++ b/src/app/util/limit.tsx @@ -1,6 +1,5 @@ import I18nNode from "@app/components/common/I18nNode" -import { t } from "@app/locale" -import { locale } from "@i18n" +import { t, locale } from "@app/locale" import { VerificationPair } from "@service/limit-service/verification/common" import verificationProcessor from "@service/limit-service/verification/processor" import { getCssVariable } from "@util/style" @@ -17,7 +16,7 @@ import { hasLimited, dateMinute2Idx, skipToday } from "@util/limit" export async function judgeVerificationRequired(item: timer.limit.Item): Promise { const { visitTime, periods, enabled } = item || {} if (!enabled || skipToday(item)) return false - // Daily + // Daily or weekly if (hasLimited(item)) return true // Period if (periods?.length) { diff --git a/src/app/util/time.ts b/src/app/util/time.ts index 56479b2d..b8839f2e 100644 --- a/src/app/util/time.ts +++ b/src/app/util/time.ts @@ -6,7 +6,7 @@ */ import { t } from "@app/locale" -import { formatPeriodCommon, MILL_PER_MINUTE } from "@util/time" +import { formatPeriodCommon, MILL_PER_MINUTE, MILL_PER_SECOND } from "@util/time" /** * Convert {yyyy}{mm}{dd} to locale time @@ -48,7 +48,7 @@ export function periodFormatter(milliseconds: number, option?: PeriodFormatOptio if (format === "default") return formatPeriodCommon(milliseconds) let val: string = null if (format === "second") { - val = Math.floor(milliseconds / 1000).toFixed(0) + val = Math.floor(milliseconds / MILL_PER_SECOND).toFixed(0) } else if (format === "minute") { val = (milliseconds / MILL_PER_MINUTE).toFixed(1) } else if (format === "hour") { diff --git a/src/background/backup-scheduler.ts b/src/background/backup-scheduler.ts index 8072511f..c3bb42d6 100644 --- a/src/background/backup-scheduler.ts +++ b/src/background/backup-scheduler.ts @@ -8,6 +8,7 @@ import optionService from "@service/option-service" import alarmManager from "./alarm-manager" import processor from "@src/common/backup/processor" +import { MILL_PER_MINUTE } from "@util/time" const ALARM_NAME = 'auto-backup-data' @@ -26,7 +27,7 @@ class BackupScheduler { private handleOption(option: timer.option.BackupOption) { const { autoBackUp, backupType, autoBackUpInterval = 0 } = option || {} this.needBackup = backupType !== "none" && !!backupType && !!autoBackUp - this.interval = autoBackUpInterval * 60 * 1000 + this.interval = autoBackUpInterval * MILL_PER_MINUTE if (this.needSchedule()) { alarmManager.setInterval(ALARM_NAME, this.interval, () => this.doBackup()) } else { diff --git a/src/background/badge-manager.ts b/src/background/badge-manager.ts index 3d7dd8b6..079466a2 100644 --- a/src/background/badge-manager.ts +++ b/src/background/badge-manager.ts @@ -12,6 +12,7 @@ import StatDatabase from "@db/stat-database" import whitelistHolder from "@service/components/whitelist-holder" import optionService from "@service/option-service" import { extractHostname, isBrowserUrl } from "@util/pattern" +import { MILL_PER_HOUR, MILL_PER_MINUTE, MILL_PER_SECOND } from "@util/time" const storage = chrome.storage.local const statDatabase: StatDatabase = new StatDatabase(storage) @@ -29,14 +30,14 @@ export type BadgeLocation = { } function mill2Str(milliseconds: number) { - if (milliseconds < 60000) { + if (milliseconds < MILL_PER_MINUTE) { // no more than 1 minutes - return `${Math.round(milliseconds / 1000)}s` - } else if (milliseconds < 3600000) { + return `${Math.round(milliseconds / MILL_PER_SECOND)}s` + } else if (milliseconds < MILL_PER_HOUR) { // no more than 1 hour - return `${Math.round(milliseconds / 60000)}m` + return `${Math.round(milliseconds / MILL_PER_MINUTE)}m` } else { - return `${(milliseconds / 3600000).toFixed(1)}h` + return `${(milliseconds / MILL_PER_HOUR).toFixed(1)}h` } } diff --git a/src/background/limit-processor.ts b/src/background/limit-processor.ts index c3e13b20..18b77020 100644 --- a/src/background/limit-processor.ts +++ b/src/background/limit-processor.ts @@ -13,11 +13,11 @@ import { matches } from "@util/limit" import limitService from "@service/limit-service" import { isBrowserUrl } from "@util/pattern" import alarmManager from "./alarm-manager" -import { getStartOfDay, MILL_PER_DAY } from "@util/time" +import { getStartOfDay, MILL_PER_DAY, MILL_PER_SECOND } from "@util/time" function processLimitWaking(rules: timer.limit.Item[], tab: ChromeTab) { const { url } = tab - const anyMatch = rules.map(rule => matches(rule, url)).reduce((a, b) => a || b, false) + const anyMatch = rules.map(rule => matches(rule?.cond, url)).reduce((a, b) => a || b, false) if (!anyMatch) { return } @@ -62,11 +62,11 @@ const processMoreMinutes = async (url: string) => { const processAskHitVisit = async (item: timer.limit.Item) => { let tabs = await listTabs() - tabs = tabs?.filter(({ url }) => matches(item, url)) + tabs = tabs?.filter(({ url }) => matches(item?.cond, url)) const { visitTime = 0 } = item || {} for (const { id } of tabs) { const tabFocus = await sendMsg2Tab(id, "askVisitTime", undefined) - if (tabFocus && tabFocus > visitTime * 1000) return true + if (tabFocus && tabFocus > visitTime * MILL_PER_SECOND) return true } return false } diff --git a/src/background/version-manager/0-1-2/host-merge-initializer.ts b/src/background/version-manager/0-1-2/host-merge-initializer.ts index 8c87a2aa..a3a6e350 100644 --- a/src/background/version-manager/0-1-2/host-merge-initializer.ts +++ b/src/background/version-manager/0-1-2/host-merge-initializer.ts @@ -1,6 +1,6 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ @@ -11,7 +11,7 @@ const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local) /** * v0.1.2 - * + * * Initialize the merge rules */ export default class HostMergeInitializer implements VersionProcessor { @@ -22,12 +22,12 @@ export default class HostMergeInitializer implements VersionProcessor { process(): void { mergeRuleDatabase.add( - // Google's regional hosts + // Google's regional hosts { origin: '*.google.com.*', merged: 'google.com' }, // level-3 of .edu.cn { origin: '*.*.edu.cn', merged: 2 }, - // not merge wx2.qq.com - { origin: 'wx2.qq.com', merged: '' } + // not merge wx2.qq.com + { origin: 'wx2.qq.com', merged: '' }, ).then(() => console.log('Host merge rules initialized')) } } \ No newline at end of file diff --git a/src/common/backup/gist/compressor.ts b/src/common/backup/gist/compressor.ts index 6684b2ee..650e781b 100644 --- a/src/common/backup/gist/compressor.ts +++ b/src/common/backup/gist/compressor.ts @@ -6,7 +6,7 @@ */ import { groupBy } from "@util/array" -import { formatTime, getBirthday, parseTime } from "@util/time" +import { formatTimeYMD, getBirthday, parseTime } from "@util/time" function calcGroupKey(row: timer.stat.RowBase): string { const date = row.date @@ -48,15 +48,15 @@ export function divide2Buckets(rows: timer.stat.RowBase[]): [string, GistData][] * Calculate all the buckets between {@param startDate} and {@param endDate} */ export function calcAllBuckets(startDate: string, endDate: string) { - endDate = endDate || formatTime(new Date(), '{y}{m}{d}') + endDate = endDate || formatTimeYMD(new Date()) const result = [] const start = startDate ? parseTime(startDate) : getBirthday() const end = parseTime(endDate) while (start < end) { - result.push(formatTime(start, '{y}{m}{d}')) + result.push(formatTimeYMD(start)) start.setMonth(start.getMonth() + 1) } - const lastMonth = formatTime(end, '{y}{m}{d}') + const lastMonth = formatTimeYMD(end) !result.includes(lastMonth) && (result.push(lastMonth)) return result } diff --git a/src/common/backup/gist/coordinator.ts b/src/common/backup/gist/coordinator.ts index bfaeb0fc..bca2261b 100644 --- a/src/common/backup/gist/coordinator.ts +++ b/src/common/backup/gist/coordinator.ts @@ -11,7 +11,7 @@ import { getJsonFileContent, findTarget, getGist, createGist, updateGist, testTo import { SOURCE_CODE_PAGE } from "@util/constant/url" import { calcAllBuckets, divide2Buckets, gistData2Rows } from "./compressor" import MonthIterator from "@util/month-iterator" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" const TIMER_META_GIST_DESC = "Used for timer to save meta info. Don't change this description :)" const TIMER_DATA_GIST_DESC = "Used for timer to save stat data. Don't change this description :)" @@ -85,8 +85,8 @@ export default class GistCoordinator implements timer.backup.Coordinator async download(context: timer.backup.CoordinatorContext, startTime: Date, endTime: Date, targetCid?: string): Promise { const allYearMonth = new MonthIterator(startTime, endTime || new Date()).toArray() const result: timer.stat.RowBase[] = [] - const start = formatTime(startTime, "{y}{m}{d}") - const end = formatTime(endTime, "{y}{m}{d}") + const start = formatTimeYMD(startTime) + const end = formatTimeYMD(endTime) await Promise.all(allYearMonth.map(async yearMonth => { const filename = bucket2filename(yearMonth, targetCid || context.cid) const gist: Gist = await this.getStatGist(context) @@ -101,8 +101,6 @@ export default class GistCoordinator implements timer.backup.Coordinator return result } - - async upload(context: timer.backup.CoordinatorContext, rows: timer.stat.RowBase[]): Promise { const cid = context.cid const buckets = divide2Buckets(rows) diff --git a/src/common/backup/processor.ts b/src/common/backup/processor.ts index df708325..a6e06000 100644 --- a/src/common/backup/processor.ts +++ b/src/common/backup/processor.ts @@ -10,7 +10,7 @@ import metaService from "@service/meta-service" import optionService from "@service/option-service" import statService from "@service/stat-service" import { judgeVirtualFast } from "@util/pattern" -import { formatTime, getBirthday } from "@util/time" +import { formatTimeYMD, getBirthday } from "@util/time" import GistCoordinator from "./gist/coordinator" import ObsidianCoordinator from "./obsidian/coordinator" @@ -123,7 +123,7 @@ async function syncFull( } return { ts: end.getTime(), - date: formatTime(end, '{y}{m}{d}') + date: formatTimeYMD(end), } } @@ -219,8 +219,8 @@ class Processor { // 1. init context const context: timer.backup.CoordinatorContext = await new CoordinatorContextWrapper(localCid, auth, ext, type).init() // 2. query all clients, and filter them - let startStr = start ? formatTime(start, '{y}{m}{d}') : undefined - let endStr = end ? formatTime(end, '{y}{m}{d}') : undefined + let startStr = start ? formatTimeYMD(start) : undefined + let endStr = end ? formatTimeYMD(end) : undefined const allClients = (await coordinator.listAllClients(context)) .filter(c => filterClient(c, excludeLocal, localCid, startStr, endStr)) .filter(c => !specCid || c.id === specCid) diff --git a/src/content-script/limit/common.ts b/src/content-script/limit/common.ts index aef6f28f..d8c30e2a 100644 --- a/src/content-script/limit/common.ts +++ b/src/content-script/limit/common.ts @@ -9,13 +9,14 @@ export type LimitReason = export type LimitType = | "DAILY" + | "WEEKLY" | "VISIT" | "PERIOD" export interface MaskModal { - addReason(reason: LimitReason): void - removeReason(reason: LimitReason): void - removeReasonsByType(type: LimitType): void + addReason(...reasons: LimitReason[]): void + removeReason(...reasons: LimitReason[]): void + removeReasonsByType(...types: LimitType[]): void addDelayHandler(handler: () => void): void } diff --git a/src/content-script/limit/index.ts b/src/content-script/limit/index.ts index e5100a6f..6c502bbc 100644 --- a/src/content-script/limit/index.ts +++ b/src/content-script/limit/index.ts @@ -1,6 +1,6 @@ import { MaskModal, ModalContext, Processor } from "./common" import ModalInstance from "./modal" -import DailyProcessor from "./processor/daily-processor" +import MessageAdaptor from "./processor/message-adaptor" import VisitProcessor from "./processor/visit-processor" import PeriodProcessor from "./processor/period-processor" import { onRuntimeMessage } from "@api/chrome/runtime" @@ -11,7 +11,7 @@ export default async function processLimit(url: string) { const context: ModalContext = { modal, url } const processors: Processor[] = [ - new DailyProcessor(context), + new MessageAdaptor(context), new PeriodProcessor(context), new VisitProcessor(context), ] diff --git a/src/content-script/limit/modal/components/Footer.tsx b/src/content-script/limit/modal/components/Footer.tsx index 50940c1b..5819acb7 100644 --- a/src/content-script/limit/modal/components/Footer.tsx +++ b/src/content-script/limit/modal/components/Footer.tsx @@ -10,7 +10,7 @@ import { Plus, Timer } from "@element-plus/icons-vue" import { sendMsg2Runtime } from "@api/chrome/runtime" import { TAG_NAME } from "@cs/limit/element" -const DELAY_ENABLED: LimitType[] = ['DAILY', 'VISIT'] +const DELAY_ENABLED: LimitType[] = ['DAILY', 'VISIT', 'WEEKLY'] async function handleMore5Minutes(rule: timer.limit.Item, callback: () => void) { let promise: Promise = undefined diff --git a/src/content-script/limit/modal/components/Reason.tsx b/src/content-script/limit/modal/components/Reason.tsx index 7dc91567..44838f96 100644 --- a/src/content-script/limit/modal/components/Reason.tsx +++ b/src/content-script/limit/modal/components/Reason.tsx @@ -3,7 +3,7 @@ import { useReason, useRule } from "../context" import { t } from "@cs/locale" import { ElDescriptions, ElDescriptionsItem } from "element-plus" import { useRequest } from "@hooks" -import { formatPeriodCommon } from "@util/time" +import { formatPeriodCommon, MILL_PER_SECOND } from "@util/time" import { period2Str } from "@util/limit" const _default = defineComponent(() => { @@ -27,7 +27,7 @@ const _default = defineComponent(() => { { reason.value?.type === 'DAILY' && <> msg.limit.item.time)} labelAlign="right"> - {formatPeriodCommon(rule.value?.time * 1000) || '-'} + {formatPeriodCommon(rule.value?.time * MILL_PER_SECOND) || '-'} {(!!reason.value?.allowDelay || !!reason.value?.delayCount) && ( msg.limit.item.delayCount)} labelAlign="right"> @@ -39,10 +39,25 @@ const _default = defineComponent(() => { } + { + reason.value?.type === 'WEEKLY' && <> + msg.limit.item.weekly)} labelAlign="right"> + {formatPeriodCommon(rule.value?.weekly * MILL_PER_SECOND) || '-'} + + {(!!reason.value?.allowDelay || !!reason.value?.delayCount) && ( + msg.limit.item.delayCount)} labelAlign="right"> + {rule.value?.weeklyDelayCount ?? 0} + + )} + msg.limit.item.wasteWeekly)} labelAlign="right"> + {formatPeriodCommon(rule.value?.weeklyWaste) || '-'} + + + } { reason.value?.type === 'VISIT' && <> msg.limit.item.visitTime)} labelAlign="right"> - {formatPeriodCommon(rule.value?.visitTime * 1000) || '-'} + {formatPeriodCommon(rule.value?.visitTime * MILL_PER_SECOND) || '-'} {(!!reason.value?.allowDelay || !!reason.value?.delayCount) && ( msg.limit.item.delayCount)} labelAlign="right"> diff --git a/src/content-script/limit/modal/index.ts b/src/content-script/limit/modal/index.ts index 09339f03..ce85687a 100644 --- a/src/content-script/limit/modal/index.ts +++ b/src/content-script/limit/modal/index.ts @@ -51,6 +51,7 @@ const TYPE_SORT: { [reason in LimitType]: number } = { PERIOD: 0, VISIT: 1, DAILY: 2, + WEEKLY: 3, } const createHeader = () => { @@ -79,22 +80,30 @@ class ModalInstance implements MaskModal { this.url = url } - addReason(reason: LimitReason): void { - const exist = this.reasons.some(r => isSameReason(r, reason)) - if (exist) return - this.reasons.push(reason) + addReason(...reasons2Add: LimitReason[]): void { + reasons2Add = reasons2Add.filter(r => { + const anyExist = this.reasons?.some(reason => isSameReason(r, reason)) + return !anyExist + }) + if (!reasons2Add?.length) return + this.reasons.push(...reasons2Add) // Sort this.reasons.sort((a, b) => TYPE_SORT[a.type] - TYPE_SORT[b.type]) this.refresh() } - removeReason(reason: LimitReason): void { - this.reasons = this.reasons?.filter(r => !isSameReason(r, reason)) + removeReason(...reasons2Remove: LimitReason[]): void { + if (!reasons2Remove?.length) return + this.reasons = this.reasons?.filter(reason => { + const anyRemove = reasons2Remove.some(r => isSameReason(reason, r)) + return !anyRemove + }) this.refresh() } - removeReasonsByType(type: LimitType): void { - this.reasons = this.reasons?.filter(r => r.type !== type) + removeReasonsByType(...types: LimitType[]): void { + if (!types?.length) return + this.reasons = this.reasons?.filter(r => !types?.includes(r.type)) this.refresh() } diff --git a/src/content-script/limit/processor/daily-processor.ts b/src/content-script/limit/processor/daily-processor.ts deleted file mode 100644 index 25c6d981..00000000 --- a/src/content-script/limit/processor/daily-processor.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { hasLimited, matches } from "@util/limit" -import { LimitReason, ModalContext, Processor } from "../common" -import { sendMsg2Runtime } from "@api/chrome/runtime" - -const cvtItem2Reason = (item: timer.limit.Item): LimitReason => { - const { cond, allowDelay, id, delayCount } = item - return { type: "DAILY", cond, allowDelay, id, delayCount } -} - -class DailyProcessor implements Processor { - private context: ModalContext - - constructor(context: ModalContext) { - this.context = context - } - - handleMsg(code: timer.mq.ReqCode, data: unknown): timer.mq.Response | Promise { - let items = data as timer.limit.Item[] - if (code === "limitTimeMeet") { - if (!items?.length) { - return { code: "fail" } - } - items.filter(item => matches(item, this.context.url)) - .map(cvtItem2Reason) - .forEach(reason => this.context.modal.addReason(reason)) - return { code: "success" } - } else if (code === "limitChanged") { - this.context.modal.removeReasonsByType("DAILY") - items?.filter?.(i => hasLimited(i)) - ?.map(cvtItem2Reason) - ?.forEach(reason => this.context.modal.addReason(reason)) - return { code: "success" } - } else if (code === "limitWaking") { - items?.map(cvtItem2Reason) - ?.forEach(reason => this.context.modal.removeReason(reason)) - return { code: "success" } - } - return { code: "ignore" } - } - - async init(): Promise { - this.initRules?.() - this.context.modal?.addDelayHandler(() => this.initRules()) - } - - async initRules(): Promise { - this.context.modal?.removeReasonsByType?.('DAILY') - const limitedRules: timer.limit.Item[] = await sendMsg2Runtime('cs.getLimitedRules', this.context.url) - - limitedRules?.forEach(({ cond, allowDelay, id, delayCount }) => { - const reason: LimitReason = { type: "DAILY", cond, allowDelay, id, delayCount } - this.context.modal.addReason(reason) - }) - } -} - -export default DailyProcessor \ No newline at end of file diff --git a/src/content-script/limit/processor/message-adaptor.ts b/src/content-script/limit/processor/message-adaptor.ts new file mode 100644 index 00000000..12bc964d --- /dev/null +++ b/src/content-script/limit/processor/message-adaptor.ts @@ -0,0 +1,66 @@ +import { hasDailyLimited, hasWeeklyLimited, matches } from "@util/limit" +import { LimitReason, ModalContext, Processor } from "../common" +import { sendMsg2Runtime } from "@api/chrome/runtime" + +const cvtItem2AddReason = (item: timer.limit.Item): LimitReason[] => { + const { cond, allowDelay, id, delayCount, weeklyDelayCount } = item + const reasons2Add: LimitReason[] = [] + hasDailyLimited(item) && reasons2Add.push({ type: "DAILY", cond, allowDelay, id, delayCount }) + hasWeeklyLimited(item) && reasons2Add.push({ type: 'WEEKLY', cond, allowDelay, id, delayCount: weeklyDelayCount }) + return reasons2Add +} + +const cvtItem2RemoveReason = (item: timer.limit.Item): LimitReason[] => { + const { cond, allowDelay, id, delayCount, weeklyDelayCount } = item + const reasons2Remove: LimitReason[] = [] + !hasDailyLimited(item) && reasons2Remove.push({ type: 'DAILY', cond, allowDelay, id, delayCount }) + !hasWeeklyLimited(item) && reasons2Remove.push({ type: 'WEEKLY', cond, allowDelay, id, delayCount: weeklyDelayCount }) + return reasons2Remove +} + +class MessageAdaptor implements Processor { + private context: ModalContext + + constructor(context: ModalContext) { + this.context = context + } + + handleMsg(code: timer.mq.ReqCode, data: unknown): timer.mq.Response | Promise { + let items = data as timer.limit.Item[] + if (code === "limitTimeMeet") { + if (!items?.length) { + return { code: "fail" } + } + items.filter(item => matches(item?.cond, this.context.url)) + .flatMap(cvtItem2AddReason) + .forEach(reason => reason && this.context.modal.addReason(reason)) + return { code: "success" } + } else if (code === "limitChanged") { + this.context.modal.removeReasonsByType("DAILY", "WEEKLY") + items?.flatMap(cvtItem2AddReason) + ?.forEach(reason => reason && this.context.modal.addReason(reason)) + return { code: "success" } + } else if (code === "limitWaking") { + const reasons2Remove = items?.flatMap(cvtItem2RemoveReason) + reasons2Remove?.length && this.context.modal.removeReason(...reasons2Remove) + return { code: "success" } + } + return { code: "ignore" } + } + + async init(): Promise { + this.initRules?.() + this.context.modal?.addDelayHandler(() => this.initRules()) + } + + async initRules(): Promise { + this.context.modal?.removeReasonsByType?.('DAILY', 'WEEKLY') + const limitedRules: timer.limit.Item[] = await sendMsg2Runtime('cs.getLimitedRules', this.context.url) + + limitedRules + ?.flatMap?.(cvtItem2AddReason) + ?.forEach(reason => this.context.modal.addReason(reason)) + } +} + +export default MessageAdaptor \ No newline at end of file diff --git a/src/content-script/limit/processor/period-processor.ts b/src/content-script/limit/processor/period-processor.ts index b4bada09..199e8e3b 100644 --- a/src/content-script/limit/processor/period-processor.ts +++ b/src/content-script/limit/processor/period-processor.ts @@ -1,6 +1,7 @@ import { sendMsg2Runtime } from "@api/chrome/runtime" import { LimitReason, ModalContext, Processor } from "../common" import { date2Idx } from "@util/limit" +import { MILL_PER_SECOND } from "@util/time" function processRule(rule: timer.limit.Rule, nowSeconds: number, context: ModalContext): number[] { const { cond, periods, id } = rule @@ -11,11 +12,11 @@ function processRule(rule: timer.limit.Rule, nowSeconds: number, context: ModalC const reason: LimitReason = { id, cond, type: "PERIOD" } const timers = [] if (nowSeconds < startSeconds) { - timers.push(setInterval(() => context.modal.addReason(reason), (startSeconds - nowSeconds) * 1000)) - timers.push(setInterval(() => context.modal.removeReason(reason), (endSeconds - nowSeconds) * 1000)) + timers.push(setInterval(() => context.modal.addReason(reason), (startSeconds - nowSeconds) * MILL_PER_SECOND)) + timers.push(setInterval(() => context.modal.removeReason(reason), (endSeconds - nowSeconds) * MILL_PER_SECOND)) } else if (nowSeconds >= startSeconds && nowSeconds <= endSeconds) { context.modal.addReason(reason) - timers.push(setInterval(() => context.modal.removeReason(reason), (endSeconds - nowSeconds) * 1000)) + timers.push(setInterval(() => context.modal.removeReason(reason), (endSeconds - nowSeconds) * MILL_PER_SECOND)) } return timers }) diff --git a/src/content-script/limit/processor/visit-processor.ts b/src/content-script/limit/processor/visit-processor.ts index 28c12f06..6b13b278 100644 --- a/src/content-script/limit/processor/visit-processor.ts +++ b/src/content-script/limit/processor/visit-processor.ts @@ -2,6 +2,7 @@ import TrackerClient from "@src/background/timer/client" import { ModalContext, Processor } from "../common" import { sendMsg2Runtime } from "@api/chrome/runtime" import { DELAY_MILL } from "@util/limit" +import { MILL_PER_SECOND } from "@util/time" class VisitProcessor implements Processor { @@ -28,7 +29,7 @@ class VisitProcessor implements Processor { hasLimited(rule: timer.limit.Rule): boolean { const { visitTime } = rule || {} if (!visitTime) return false - return visitTime * 1000 + this.delayCount * DELAY_MILL < this.focusTime + return visitTime * MILL_PER_SECOND + this.delayCount * DELAY_MILL < this.focusTime } async handleTracker(data: timer.stat.Event) { diff --git a/src/database/common/constant.ts b/src/database/common/constant.ts index 5a6de176..ec518eae 100644 --- a/src/database/common/constant.ts +++ b/src/database/common/constant.ts @@ -1,13 +1,13 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ /** * Prefix of remain words - * + * * @since 0.0.5 */ export const REMAIN_WORD_PREFIX = '__timer__' @@ -23,8 +23,3 @@ export const WHITELIST_KEY = REMAIN_WORD_PREFIX + 'WHITELIST' * @since 0.6.0 */ export const META_KEY = REMAIN_WORD_PREFIX + 'META' - -/** - * Date format for storage - */ -export const DATE_FORMAT = '{y}{m}{d}' \ No newline at end of file diff --git a/src/database/limit-database.ts b/src/database/limit-database.ts index 8e20adc4..fc670e11 100644 --- a/src/database/limit-database.ts +++ b/src/database/limit-database.ts @@ -5,11 +5,23 @@ * https://opensource.org/licenses/MIT */ +import { formatTimeYMD, MILL_PER_DAY } from "@util/time" import BaseDatabase from "./common/base-database" import { REMAIN_WORD_PREFIX } from "./common/constant" const KEY = REMAIN_WORD_PREFIX + 'LIMIT' +type DateRecords = { + [date: string]: { + mill: number + delay?: number + } +} + +type LimitRecord = timer.limit.Rule & { + records: DateRecords +} + type ItemValue = { /** * ID @@ -27,6 +39,10 @@ type ItemValue = { * Limited time, second */ t: number + /** + * Limited time weekly, second + */ + wt?: number /** * Limited time per visit, second */ @@ -41,15 +57,18 @@ type ItemValue = { e: boolean /** * Latest date + * + * @deprecated use @see ItemValue.r */ - d: string + d?: string /** * Wasted time, milliseconds + * + * @deprecated use @see ItemValue.r */ - w: number + w?: number /** * Allow to delay - * @since 0.4.0 */ ad: boolean /** @@ -58,14 +77,71 @@ type ItemValue = { wd?: number[] /** * Delay date - * @since 2.6.7 + * + * @deprecated use @see ItemValue.r */ dd?: string /** * Delay count - * @since 2.6.7 + * + * @deprecated use @see ItemValue.r */ dc?: number + /** + * Date records + */ + r?: { + [date: string]: { + /** + * Milliseconds + */ + m: number + /** + * Delay count + */ + d?: number + } + } +} + +/** + * @deprecated v2.4.1 + */ +const migrateOldRecords = (item: ItemValue, records: DateRecords): void => { + const { d, w, dd, dc } = item + if (d) { + const val = records[d] || { mill: 0 } + val.mill = w ?? 0 + records[d] = val + } + if (dd) { + const val = records[dd] || { mill: 0 } + val.delay = dc ?? 0 + records[dd] = val + } +} + +const cvtItem2Rec = (item: ItemValue): LimitRecord => { + const { i, n, c, t, v, p, e, ad, wd, wt, r } = item + const records: DateRecords = {} + if (r) { + Object.entries(r).forEach?.(([date, { m, d }]) => records[date] = { mill: m, delay: d }) + } else { + migrateOldRecords(item, records) + } + return { + id: i, + name: n, + cond: c, + time: t, + weekly: wt, + visitTime: v, + periods: p?.map(i => [i?.[0], i?.[1]]), + enabled: e, + allowDelay: !!ad, + weekdays: wd, + records: records, + } } type Items = Record @@ -76,10 +152,16 @@ function migrate(exist: Items, toMigrate: any) { const id = idBase + idx const itemValue: ItemValue = value as ItemValue const { c, n, t, e, ad, d, w, v, p } = itemValue - exist[id] = { i: id, c, n, t, e: !!e, ad: !!ad, d, w: w || 0, v, p } + exist[id] = { + i: id, c, n, t, e: !!e, ad: !!ad, v, p, + r: d ? { [d]: { m: w } } : {}, + } }) } +/** + * @deprecated Compatible for old items without ID + */ const compatibleOldItems = (items: Items): Items => { const newItems: Items = {} Object.entries(items).forEach(([c, v], idx) => { @@ -108,30 +190,30 @@ class LimitDatabase extends BaseDatabase { } private update(items: Items): Promise { + const days10Ago = new Date(Date.now() - MILL_PER_DAY * 10) + const days10AgoStr = formatTimeYMD(days10Ago) + // Clear early date + Object.values(items).forEach(item => { + delete item.w + delete item.d + delete item.dc + delete item.dd + const records = item.r + if (!records) return + const keys2Del = Object.keys(records).filter(k => k <= days10AgoStr) + keys2Del.forEach(k => delete records[k]) + }) return this.setByKey(KEY, items) } - async all(): Promise { + async all(): Promise { const items = await this.getItems() - return Object.values(items).map(({ i, n, c, t, v, p, e, ad, w, d, wd, dd, dc }) => ({ - id: i, - name: n, - cond: c, - time: t, - visitTime: v, - periods: p?.map(i => [i?.[0], i?.[1]]), - enabled: e, - allowDelay: !!ad, - wasteTime: w, - latestDate: d, - weekdays: wd, - delay: { count: dc, date: dd } - })) + return Object.values(items).map(cvtItem2Rec) } async save(data: timer.limit.Rule, rewrite?: boolean): Promise { const items = await this.getItems() - let { id, name, cond, time, enabled, allowDelay, visitTime, periods, weekdays } = data + let { id, name, cond, time, weekly, enabled, allowDelay, visitTime, periods, weekdays } = data if (!id) { const lastId = Object.values(items) .map(e => e.i) @@ -145,7 +227,9 @@ class LimitDatabase extends BaseDatabase { // Can be overridden by existing d: '', w: 0, ...(existItem || {}), - i: id, n: name, c: cond, t: time, e: enabled, ad: allowDelay, v: visitTime, p: periods, wd: weekdays + i: id, n: name, c: cond, wd: weekdays, + e: enabled, ad: allowDelay, + t: time, wt: weekly, v: visitTime, p: periods, } await this.update(items) return id @@ -159,11 +243,13 @@ class LimitDatabase extends BaseDatabase { async updateWaste(date: string, toUpdate: { [id: number]: number }): Promise { const items = await this.getItems() - Object.entries(toUpdate).forEach(([id, waste]) => { + Object.entries(toUpdate).forEach(([k, waste]) => { + const id = parseInt(k) const entry = items[id] if (!entry) return - entry.d = date - entry.w = waste + const records = entry.r = entry.r || {} + const record = records[date] = records[date] || { m: 0 } + record.m = waste }) await this.update(items) } @@ -173,8 +259,9 @@ class LimitDatabase extends BaseDatabase { toUpdate?.forEach(({ id, delayCount }) => { const entry = items[id] if (!entry) return - entry.dc = delayCount - entry.dd = date + const records = entry.r = entry.r || {} + const record = records[date] = records[date] || { m: 0 } + record.d = delayCount }) await this.update(items) } @@ -186,6 +273,13 @@ class LimitDatabase extends BaseDatabase { await this.update(items) } + async updateEnabled(id: number, enabled: boolean) { + const items = await this.getItems() + if (!items[id]) return + items[id].e = !!enabled + await this.update(items) + } + async importData(data: any): Promise { let toImport = data[KEY] as Items // Not import diff --git a/src/database/stat-database/filter.ts b/src/database/stat-database/filter.ts index 8639d596..e9b6f3b5 100644 --- a/src/database/stat-database/filter.ts +++ b/src/database/stat-database/filter.ts @@ -1,6 +1,5 @@ -import { DATE_FORMAT } from "@db/common/constant" import { judgeVirtualFast } from "@util/pattern" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" import StatDatabase, { StatCondition } from "." type _StatCondition = StatCondition & { @@ -82,7 +81,7 @@ function processDateCondition(cond: _StatCondition, paramDate: Date | [Date, Dat if (paramDate instanceof Date) { cond.useExactDate = true - cond.exactDateStr = formatTime(paramDate as Date, DATE_FORMAT) + cond.exactDateStr = formatTimeYMD(paramDate as Date) } else { let startDate: Date = undefined let endDate: Date = undefined @@ -90,8 +89,8 @@ function processDateCondition(cond: _StatCondition, paramDate: Date | [Date, Dat dateArr && dateArr.length >= 2 && (endDate = dateArr[1]) dateArr && dateArr.length >= 1 && (startDate = dateArr[0]) cond.useExactDate = false - startDate && (cond.startDateStr = formatTime(startDate, DATE_FORMAT)) - endDate && (cond.endDateStr = formatTime(endDate, DATE_FORMAT)) + startDate && (cond.startDateStr = formatTimeYMD(startDate)) + endDate && (cond.endDateStr = formatTimeYMD(endDate)) } } diff --git a/src/database/stat-database/index.ts b/src/database/stat-database/index.ts index 0f4f75d4..d8d3d79a 100644 --- a/src/database/stat-database/index.ts +++ b/src/database/stat-database/index.ts @@ -6,9 +6,9 @@ */ import { log } from "../../common/logger" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" import BaseDatabase from "../common/base-database" -import { DATE_FORMAT, REMAIN_WORD_PREFIX } from "../common/constant" +import { REMAIN_WORD_PREFIX } from "../common/constant" import { createZeroResult, mergeResult, isNotZeroResult } from "@util/stat" import { judgeVirtualFast } from "@util/pattern" import { filter } from "./filter" @@ -61,7 +61,7 @@ function mergeMigration(exist: timer.stat.Result | undefined, another: any) { * @param date date */ function generateKey(host: string, date: Date | string) { - const str = typeof date === 'object' ? formatTime(date as Date, DATE_FORMAT) : date + const str = typeof date === 'object' ? formatTimeYMD(date as Date) : date return str + host } @@ -111,7 +111,7 @@ class StatDatabase extends BaseDatabase { async accumulateBatch(data: timer.stat.ResultSet, date: Date): Promise { const hosts = Object.keys(data) if (!hosts.length) return - const dateStr = formatTime(date, DATE_FORMAT) + const dateStr = formatTimeYMD(date) const keys: { [host: string]: string } = {} hosts.forEach(host => keys[host] = generateKey(host, dateStr)) @@ -209,8 +209,8 @@ class StatDatabase extends BaseDatabase { * @since 0.0.7 */ async deleteByUrlBetween(host: string, start?: Date, end?: Date): Promise { - const startStr = start ? formatTime(start, DATE_FORMAT) : undefined - const endStr = end ? formatTime(end, DATE_FORMAT) : undefined + const startStr = start ? formatTimeYMD(start) : undefined + const endStr = end ? formatTimeYMD(end) : undefined const dateFilter = (date: string) => (startStr ? startStr <= date : true) && (endStr ? date <= endStr : true) const items = await this.refresh() diff --git a/src/i18n/message/app/limit-resource.json b/src/i18n/message/app/limit-resource.json index e7f3f21c..9e360b43 100644 --- a/src/i18n/message/app/limit-resource.json +++ b/src/i18n/message/app/limit-resource.json @@ -7,6 +7,9 @@ "condition": "限制网址", "time": "每日最长浏览时间", "waste": "今日浏览时长", + "weekly": "每周最长浏览时间", + "weekStartInfo": "每周的第一天是:{weekStart},你可以在统计选项中修改该值", + "wasteWeekly": "本周浏览时长", "delayCount": "延时次数", "detail": "规则详情", "visitTime": "单次访问最长浏览时间", @@ -109,6 +112,9 @@ "condition": "Restricted URL", "time": "Daily limit", "waste": "Browsed today", + "weekly": "Weekly limit", + "weekStartInfo": "The first day of each week is: {weekStart}, you can change this value in the statistics options", + "wasteWeekly": "Browsed this week", "delayCount": "Delay count", "detail": "Rule detail", "visitTime": "Limit per visit", diff --git a/src/i18n/message/app/limit.ts b/src/i18n/message/app/limit.ts index 7468296c..d9da06eb 100644 --- a/src/i18n/message/app/limit.ts +++ b/src/i18n/message/app/limit.ts @@ -21,13 +21,16 @@ export type LimitMessage = { name: string condition: string time: string + waste: string + weekly: string + wasteWeekly: string + weekStartInfo: string visitTime: string period: string enabled: string effectiveDay: string delayAllowed: string delayAllowedInfo: string - waste: string delayCount: string detail: string operation: string diff --git a/src/i18n/message/app/option-resource.json b/src/i18n/message/app/option-resource.json index bb71ff53..b1325428 100644 --- a/src/i18n/message/app/option-resource.json +++ b/src/i18n/message/app/option-resource.json @@ -7,9 +7,7 @@ "max": "只显示前 {input} 条数据,剩下的条目合并显示", "defaultMergeDomain": "{input} 打开时合并子域名", "defaultDisplay": "打开时显示 {duration} {type}", - "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名", - "weekStart": "每周的第一天 {input}", - "weekStartAsNormal": "按照惯例" + "displaySiteName": "{input} 显示时是否使用 {siteName} 来代替域名" }, "appearance": { "title": "外观", @@ -53,7 +51,9 @@ "siteName": "网站的名称", "siteNameUsage": "数据只存放在本地,将代替域名用于展示,增加辨识度。当然您可以自定义每个网站的名称", "fileAccessDisabled": "目前不允许访问文件网址,请先在管理界面开启", - "fileAccessFirefox": "很抱歉,该功能在 Firefox 中不支持" + "fileAccessFirefox": "很抱歉,该功能在 Firefox 中不支持", + "weekStart": "每周的第一天 {input}", + "weekStartAsNormal": "按照惯例" }, "dailyLimit": { "prompt": "受限时显示的提示文本 {input}", @@ -127,9 +127,7 @@ "max": "隻顯示前 {input} 條數據,剩下的條目合並顯示", "defaultMergeDomain": "{input} 是否在開啟時合併子網域", "defaultDisplay": "開啟時顯示 {duration} {type}", - "displaySiteName": "{input} 顯示時是否使用 {siteName} 來代替域名", - "weekStart": "每週的第一天 {input}", - "weekStartAsNormal": "按照慣例" + "displaySiteName": "{input} 顯示時是否使用 {siteName} 來代替域名" }, "appearance": { "title": "外觀", @@ -173,7 +171,9 @@ "siteName": "網站的名稱", "siteNameUsage": "數據隻存放在本地,將代替域名用於展示,增加辨識度。當然您可以自定義每個網站的名稱", "fileAccessDisabled": "目前不允許存取文件 URL。請先在管理頁面啟用", - "fileAccessFirefox": "抱歉,Firefox 不支援此功能" + "fileAccessFirefox": "抱歉,Firefox 不支援此功能", + "weekStart": "每週的第一天 {input}", + "weekStartAsNormal": "按照慣例" }, "dailyLimit": { "level": { @@ -247,9 +247,7 @@ "max": "Show the first {input} data items", "defaultMergeDomain": "{input} Whether to merge subdomains on open", "defaultDisplay": "Show {duration} {type} on open", - "displaySiteName": "{input} Whether to display {siteName} instead of URL", - "weekStart": "The first day for each week {input}", - "weekStartAsNormal": "As Normal" + "displaySiteName": "{input} Whether to display {siteName} instead of URL" }, "appearance": { "title": "Appearance", @@ -293,7 +291,9 @@ "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" + "fileAccessFirefox": "Sorry, this feature is not supported in Firefox", + "weekStart": "The first day for each week {input}", + "weekStartAsNormal": "As Normal" }, "dailyLimit": { "prompt": "Prompt displayed when restricted {input}", @@ -367,9 +367,7 @@ "max": "最初の {input} 個のデータのみを表示し、残りのエントリは結合されます", "defaultMergeDomain": "{input} オープン時にサブドメインをマージ", "defaultDisplay": "開くと {duration} {type} が表示されます", - "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか", - "weekStart": "週の最初の日 {input}", - "weekStartAsNormal": "いつものように" + "displaySiteName": "{input} ホストの代わりに {siteName} を表示するかどうか" }, "appearance": { "title": "外観", @@ -413,7 +411,9 @@ "siteName": "サイト名", "siteNameUsage": "データはローカルにのみ存在し、認識を高めるためにホストの代わりに表示に使用されます。もちろん、各Webサイトの名前をカスタマイズできます。", "fileAccessDisabled": "ファイル URL へのアクセスは現在許可されていません。まず管理ページで有効にしてください。", - "fileAccessFirefox": "申し訳ありませんが、この機能はFirefoxではサポートされていません" + "fileAccessFirefox": "申し訳ありませんが、この機能はFirefoxではサポートされていません", + "weekStart": "週の最初の日 {input}", + "weekStartAsNormal": "いつものように" }, "dailyLimit": { "level": { @@ -490,9 +490,7 @@ "max": "Mostrar os primeiros {input} itens de dados", "defaultMergeDomain": "{input} Se deseja mesclar subdomínios em aberto", "defaultDisplay": "Mostrar {type} de {duration} em aberto", - "displaySiteName": "{input} Se deve exibir {siteName} em vez de URL", - "weekStart": "O primeiro dia de cada semana {input}", - "weekStartAsNormal": "Normal" + "displaySiteName": "{input} Se deve exibir {siteName} em vez de URL" }, "appearance": { "title": "Aparência", @@ -536,7 +534,9 @@ "siteName": "o nome do site", "siteNameUsage": "Os dados são armazenados apenas localmente e serão exibidos em vez da URL para aumentar o reconhecimento. F, também pode personalizar o nome de cada site.", "fileAccessDisabled": "O acesso aos URL dos ficheiros não é permitido de momento. Ative-o primeiro na página de gestão", - "fileAccessFirefox": "Lamentamos, esta funcionalidade não é compatível com o Firefox" + "fileAccessFirefox": "Lamentamos, esta funcionalidade não é compatível com o Firefox", + "weekStart": "O primeiro dia de cada semana {input}", + "weekStartAsNormal": "Normal" }, "dailyLimit": { "level": { @@ -610,9 +610,7 @@ "max": "Кількість записів для показу: {input}", "defaultMergeDomain": "{input} Об'єднувати піддомени під час відкриття", "defaultDisplay": "Дані для показу: {duration} {type}", - "displaySiteName": "{input} Показувати {siteName} замість URL-адреси", - "weekStart": "Перший день тижня: {input}", - "weekStartAsNormal": "Типово" + "displaySiteName": "{input} Показувати {siteName} замість URL-адреси" }, "appearance": { "title": "Зовнішній вигляд", @@ -656,7 +654,9 @@ "siteName": "назву сайту", "siteNameUsage": "Дані зберігаються лише локально і будуть відображатися замість URL для зручності розпізнавання. Звісно, ви також можете налаштувати назву кожного сайту.", "fileAccessDisabled": "Доступ до URL-адрес файлу наразі не дозволено. Спершу ввімкніть на сторінці керування", - "fileAccessFirefox": "На жаль, ця функція не підтримується у Firefox" + "fileAccessFirefox": "На жаль, ця функція не підтримується у Firefox", + "weekStart": "Перший день тижня: {input}", + "weekStartAsNormal": "Типово" }, "backup": { "title": "Резервне копіювання", @@ -730,9 +730,7 @@ "max": "Mostrar los primeros {input} elementos de datos", "defaultMergeDomain": "{input} Combinar subdominios al abrir", "defaultDisplay": "Mostrar {duration} {type} al abrir", - "displaySiteName": "{input} Mostrar {siteName} en lugar de la URL", - "weekStart": "El primer día de cada semana {input}", - "weekStartAsNormal": "Como normalmente" + "displaySiteName": "{input} Mostrar {siteName} en lugar de la URL" }, "appearance": { "title": "Apariencia", @@ -776,7 +774,9 @@ "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" + "fileAccessFirefox": "Lo sentimos, esta función no es compatible con Firefox", + "weekStart": "El primer día de cada semana {input}", + "weekStartAsNormal": "Como normalmente" }, "backup": { "title": "Respaldo de datos", @@ -850,9 +850,7 @@ "max": "Zeige die ersten {input} Datenelemente", "defaultMergeDomain": "{input} Subdomains beim Öffnen zusammenführen", "defaultDisplay": "{duration} {type} beim Öffnen anzeigen", - "displaySiteName": "{input} {siteName} statt URL anzeigen", - "weekStart": "Erster Tag der Woche {input}", - "weekStartAsNormal": "Wie normal" + "displaySiteName": "{input} {siteName} statt URL anzeigen" }, "appearance": { "title": "Aussehen", @@ -896,7 +894,9 @@ "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" + "fileAccessFirefox": "Leider wird diese Funktion in Firefox nicht unterstützt", + "weekStart": "Erster Tag der Woche {input}", + "weekStartAsNormal": "Wie normal" }, "backup": { "title": "Datensicherung", @@ -970,9 +970,7 @@ "max": "Afficher les {input} premiers éléments de données", "defaultMergeDomain": "{input} S'il faut fusionner les sous-domaines à l'ouverture", "defaultDisplay": "Afficher {duration} {type} à l'ouverture", - "displaySiteName": "{input} S'il faut afficher {siteName} au lieu de l'URL", - "weekStart": "Le premier jour de chaque semaine {input}", - "weekStartAsNormal": "Comme d'habitude" + "displaySiteName": "{input} S'il faut afficher {siteName} au lieu de l'URL" }, "appearance": { "title": "Apparence", @@ -1016,7 +1014,9 @@ "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" + "fileAccessFirefox": "Désolé, cette fonctionnalité n'est pas prise en charge dans Firefox", + "weekStart": "Le premier jour de chaque semaine {input}", + "weekStartAsNormal": "Comme d'habitude" }, "backup": { "title": "Sauvegarde des données", diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index 65aa15d5..7ddf82d3 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -15,8 +15,6 @@ export type OptionMessage = { defaultMergeDomain: string defaultDisplay: string displaySiteName: string - weekStart: string - weekStartAsNormal: string } appearance: { title: string @@ -55,6 +53,8 @@ export type OptionMessage = { siteName: string fileAccessDisabled: string fileAccessFirefox: string + weekStart: string + weekStartAsNormal: string } dailyLimit: { prompt: string diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 42b9c36d..f957a952 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -16,7 +16,7 @@ import TypeSelectWrapper from "./select/type-select" import statService from "@service/stat-service" import { t } from "@popup/locale" import { locale } from "@i18n" -import { getDayLength, getMonthTime, getWeekDay, getWeekTime, MILL_PER_DAY } from "@util/time" +import { getDayLength, getMonthTime, getWeekTime, MILL_PER_DAY } from "@util/time" import optionService from "@service/option-service" type FooterParam = StatQueryParam & { @@ -29,28 +29,7 @@ type DateRangeCalculator = (now: Date, weekStart: timer.option.WeekStartOption) const dateRangeCalculators: { [duration in PopupDuration]: DateRangeCalculator } = { today: now => now, - thisWeek(now, weekStart) { - const weekStartAsNormal = !weekStart || weekStart === 'default' - if (weekStartAsNormal) { - return getWeekTime(now, locale === 'zh_CN') - } else { - const weekOffset: number = weekStart as number - // Returns 0 - 6 means Monday to Sunday - const weekDayNow = getWeekDay(now, true) - const optionWeekDay = weekDayNow + 1 - let start: Date = undefined - if (optionWeekDay === weekStart) { - start = now - } else if (optionWeekDay < weekStart) { - const millDelta = (optionWeekDay + 7 - weekOffset) * MILL_PER_DAY - start = new Date(now.getTime() - millDelta) - } else { - const millDelta = (optionWeekDay - weekOffset) * MILL_PER_DAY - start = new Date(now.getTime() - millDelta) - } - return [start, now] - } - }, + thisWeek: (now, weekStart) => getWeekTime(now, weekStart, locale), thisMonth: now => [getMonthTime(now)[0], now], last30Days: now => [new Date(now.getTime() - MILL_PER_DAY * 29), now], } @@ -86,7 +65,7 @@ class FooterWrapper { } async query() { - const option = await optionService.getAllOption() as timer.option.PopupOption + const option = await optionService.getAllOption() const itemCount = option.popupMax const queryParam = this.getQueryParam(option.weekStart) const rows = await statService.select(queryParam, true) diff --git a/src/service/components/week-helper.ts b/src/service/components/week-helper.ts new file mode 100644 index 00000000..43f0870a --- /dev/null +++ b/src/service/components/week-helper.ts @@ -0,0 +1,41 @@ +import OptionDatabase from "@db/option-database" +import { locale } from "@i18n" +import { formatTimeYMD, getRealWeekStart, getWeekTime } from "@util/time" + +class WeekHelper { + private optionDb = new OptionDatabase(chrome.storage.local) + private weekStart: timer.option.WeekStartOption + private initialized: boolean = false + + private async init(): Promise { + const option = await this.optionDb.getOption() + this.weekStart = option?.weekStart + this.optionDb.addOptionChangeListener(val => this.weekStart = val?.weekStart) + this.initialized = true + } + + async getWeekDateRange(now: Date): Promise<[startDate: string, endDateOrToday: string]> { + const weekStart = await this.getWeekStart() + const [start, end] = getWeekTime(now, weekStart, locale) + return [formatTimeYMD(start), formatTimeYMD(end)] + } + + private async getWeekStart(): Promise { + if (!this.initialized) { + await this.init() + } + return this.weekStart + } + + /** + * Week start + * + * @returns 0-6 + */ + async getRealWeekStart(): Promise { + const weekStart = await this.getWeekStart() + return getRealWeekStart(weekStart, locale) - 1 + } +} + +export default new WeekHelper() \ No newline at end of file diff --git a/src/service/limit-service/index.ts b/src/service/limit-service/index.ts index 262b4bc9..97ca0697 100644 --- a/src/service/limit-service/index.ts +++ b/src/service/limit-service/index.ts @@ -6,11 +6,12 @@ */ import { listTabs, sendMsg2Tab } from "@api/chrome/tab" -import { DATE_FORMAT } from "@db/common/constant" import LimitDatabase from "@db/limit-database" import { hasLimited, matches, skipToday } from "@util/limit" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" import whitelistHolder from '../components/whitelist-holder' +import { sum } from "@util/array" +import weekHelper from "@service/components/week-helper" const storage = chrome.storage.local const db: LimitDatabase = new LimitDatabase(storage) @@ -23,17 +24,30 @@ export type QueryParam = { async function select(cond?: QueryParam): Promise { const { filterDisabled, url, id } = cond || {} - const today = formatTime(new Date(), DATE_FORMAT) + const now = new Date() + const today = formatTimeYMD(now) + const [startDate, endDate] = await weekHelper.getWeekDateRange(now) + return (await db.all()) .filter(item => filterDisabled ? item.enabled : true) .filter(item => !id || id === item?.id) - .map(({ latestDate, wasteTime, delay: { count: dc, date: dd } = {}, ...others }) => ({ - ...others, - waste: latestDate === today ? (wasteTime ?? 0) : 0, - delayCount: dd === today ? (dc ?? 0) : 0, - } satisfies timer.limit.Item)) // If use url, then test it - .filter(item => !url || matches(item, url)) + .filter(item => !url || matches(item?.cond, url)) + .map(({ records, ...others }) => { + const todayRec = records[today] + const thisWeekRec = Object.entries(records) + .filter(([k]) => k >= startDate && k <= endDate) + .map(([, v]) => v) + const weeklyWaste = sum(thisWeekRec.map(r => r.mill ?? 0)) + const weeklyDelayCount = sum(thisWeekRec.map(r => r.delay ?? 0)) + return { + ...others, + waste: todayRec?.mill ?? 0, + delayCount: todayRec?.delay ?? 0, + weeklyWaste, + weeklyDelayCount, + } satisfies timer.limit.Item + }) } /** @@ -46,16 +60,14 @@ async function noticeLimitChanged() { const effectiveItems = allItems.filter(item => item.enabled && !skipToday(item)) const tabs = await listTabs() tabs.forEach(tab => { - const limitedItems = effectiveItems.filter(item => matches(item, tab.url)) + const limitedItems = effectiveItems.filter(item => matches(item?.cond, tab.url)) sendMsg2Tab(tab?.id, 'limitChanged', limitedItems) .catch(err => console.log(err.message)) }) } async function updateEnabled(item: timer.limit.Item): Promise { - const { id, name, cond, time, enabled, allowDelay, visitTime, periods } = item - const limit: timer.limit.Rule = { id, name, cond, time, enabled, allowDelay, visitTime, periods } - await db.save(limit, true) + await db.updateEnabled(item.id, item.enabled) await noticeLimitChanged() } @@ -77,28 +89,31 @@ async function getLimited(url: string): Promise { async function getRelated(url: string): Promise { return (await select()) .filter(item => item.enabled && !skipToday(item)) - .filter(item => matches(item, url)) + .filter(item => matches(item?.cond, url)) } /** * Add time + * * @param url url * @param focusTime time, milliseconds * @returns the rules is limit cause of this operation */ -async function addFocusTime(url: string, focusTime: number) { +async function addFocusTime(url: string, focusTime: number): Promise { const allEnabled: timer.limit.Item[] = await select({ filterDisabled: true, url }) const toUpdate: { [cond: string]: number } = {} const result: timer.limit.Item[] = [] allEnabled.forEach(item => { const limitBefore = hasLimited(item) toUpdate[item.id] = item.waste += focusTime + // Fast increase + item.weeklyWaste += focusTime const limitAfter = hasLimited(item) if (!limitBefore && limitAfter) { result.push(item) } }) - await db.updateWaste(formatTime(new Date, DATE_FORMAT), toUpdate) + await db.updateWaste(formatTimeYMD(new Date()), toUpdate) return result } @@ -108,9 +123,13 @@ async function addFocusTime(url: string, focusTime: number) { async function moreMinutes(url: string): Promise { const rules = (await select({ url: url, filterDisabled: true })) .filter(item => hasLimited(item) && item.allowDelay) - rules.forEach(rule => rule.delayCount = (rule.delayCount ?? 0) + 1) + rules.forEach(rule => { + rule.delayCount = (rule.delayCount ?? 0) + 1 + // Fast increase + rule.weeklyDelayCount = (rule.weeklyDelayCount ?? 0) + 1 + }) - const date = formatTime(new Date(), DATE_FORMAT) + const date = formatTimeYMD(new Date()) await db.updateDelayCount(date, rules) return rules.filter(r => !hasLimited(r)) } diff --git a/src/util/constant/limit.ts b/src/util/constant/limit.ts deleted file mode 100644 index c9709502..00000000 --- a/src/util/constant/limit.ts +++ /dev/null @@ -1 +0,0 @@ -export const DELAY_MILL = 5 * 60 * 1000 \ No newline at end of file diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index becff11b..354d5fa8 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -16,7 +16,6 @@ export function defaultPopup(): timer.option.PopupOption { * Change the default value to 'true' since v0.5.4 */ displaySiteName: true, - weekStart: 'default', defaultMergeDomain: false, } } @@ -41,6 +40,7 @@ export function defaultStatistics(): timer.option.StatisticsOption { return { collectSiteName: true, countLocalFiles: true, + weekStart: 'default', } } diff --git a/src/util/date-iterator.ts b/src/util/date-iterator.ts index 8934fe81..bd54932d 100644 --- a/src/util/date-iterator.ts +++ b/src/util/date-iterator.ts @@ -5,7 +5,7 @@ * https://opensource.org/licenses/MIT */ -import { MILL_PER_DAY, formatTime, isSameDay } from "./time" +import { MILL_PER_DAY, formatTimeYMD, isSameDay } from "./time" /** * Iterate from the {@param start} to the {@param end} @@ -31,7 +31,7 @@ export default class DateIterator { next(): IteratorResult { if (this.hasNext()) { - const value = formatTime(this.cursor, '{y}{m}{d}') + const value = formatTimeYMD(this.cursor) this.cursor = new Date(this.cursor.getTime() + MILL_PER_DAY) return { value, diff --git a/src/util/lang.ts b/src/util/lang.ts index 83cb5aff..b7f49933 100644 --- a/src/util/lang.ts +++ b/src/util/lang.ts @@ -17,9 +17,9 @@ export const deepCopy = (obj: T): T => { deep[k] = new Map(v) } else if (v instanceof Date) { deep[k] = new Date(v.getTime()) - } else if (v instanceof Proxy) { + } else { // Ignored type - deep[k] = v + deep[k] = deepCopy(v) } }) return deep as T diff --git a/src/util/limit.ts b/src/util/limit.ts index f08295e7..1d91d1af 100644 --- a/src/util/limit.ts +++ b/src/util/limit.ts @@ -1,19 +1,29 @@ -import { getWeekDay } from "./time" +import { getWeekDay, MILL_PER_MINUTE, MILL_PER_SECOND } from "./time" -export const DELAY_MILL = 5 * 60 * 1000 +export const DELAY_MILL = 5 * MILL_PER_MINUTE -export function matches(item: timer.limit.Item, url: string): boolean { - return item?.cond?.some?.( +export function matches(cond: timer.limit.Item['cond'], url: string): boolean { + return cond?.some?.( c => new RegExp(`^${(c || '').split('*').join('.*')}`).test(url) ) } export function hasLimited(item: timer.limit.Item): boolean { + return hasDailyLimited(item) || hasWeeklyLimited(item) +} + +export function hasDailyLimited(item: timer.limit.Item): boolean { const { time, waste = 0, delayCount = 0 } = item || {} if (!time) return false return waste >= time * 1000 + delayCount * DELAY_MILL } +export function hasWeeklyLimited(item: timer.limit.Item): boolean { + const { weekly, weeklyWaste = 0, weeklyDelayCount = 0 } = item || {} + if (!weekly) return false + return weeklyWaste >= weekly * 1000 + weeklyDelayCount * DELAY_MILL +} + export function skipToday(item: timer.limit.Item): boolean { const weekdays = item?.weekdays const weekdayLen = weekdays?.length diff --git a/src/util/time.ts b/src/util/time.ts index 3ccc2193..8b9c0f2a 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -55,6 +55,10 @@ export function formatTime(time: Date | string | number, cFormat?: string) { return timeStr } +export function formatTimeYMD(time: Date | string | number) { + return formatTime(time, '{y}{m}{d}') +} + /** * Format milliseconds for display */ @@ -129,11 +133,34 @@ export function isSameDay(a: Date, b: Date): boolean { * * @since 0.6.0 */ -export function getWeekTime(now: Date, isChinese: boolean): [Date, Date] { - const date = new Date(now) - const nowWeekday = getWeekDay(date, isChinese) - const startTime = new Date(date.getFullYear(), date.getMonth(), date.getDate() - nowWeekday) - return [new Date(startTime), now] +export function getWeekTime(now: Date, weekStart: timer.option.WeekStartOption, locale: timer.Locale): [Date, Date] { + weekStart = getRealWeekStart(weekStart, locale) + // Returns 0 - 6 means Monday to Sunday + const weekDayNow = getWeekDay(now, true) + const optionWeekDay = weekDayNow + 1 + let start: Date = undefined + if (optionWeekDay === weekStart) { + start = now + } else if (optionWeekDay < weekStart) { + const millDelta = (optionWeekDay + 7 - weekStart) * MILL_PER_DAY + start = new Date(now.getTime() - millDelta) + } else { + const millDelta = (optionWeekDay - weekStart) * MILL_PER_DAY + start = new Date(now.getTime() - millDelta) + } + return [start, now] +} + +/** + * return 1-7 + */ +export function getRealWeekStart(weekStart: timer.option.WeekStartOption, locale: timer.Locale): number { + weekStart = weekStart ?? 'default' + if (weekStart === 'default') { + return locale === 'zh_CN' ? 1 : 7 + } else { + return weekStart + } } /** @@ -234,14 +261,13 @@ export function getDayLength(dateStart: Date, dateEnd: Date): number { * [20221110, 20221111] if 2022-11-10 08:00:00 to 2022-11-11 00:00:01 */ export function getAllDatesBetween(dateStart: Date, dateEnd: Date): string[] { - const format = '{y}{m}{d}' let cursor = new Date(dateStart) let dates = [] do { - dates.push(formatTime(cursor, format)) + dates.push(formatTimeYMD(cursor)) cursor = new Date(cursor.getTime() + MILL_PER_DAY) } while (cursor.getTime() < dateEnd.getTime()) - isSameDay(cursor, dateEnd) && dates.push(formatTime(dateEnd, format)) + isSameDay(cursor, dateEnd) && dates.push(formatTimeYMD(dateEnd)) return dates } diff --git a/test/database/limit-database.test.ts b/test/database/limit-database.test.ts index 5fb3e82d..be105e30 100644 --- a/test/database/limit-database.test.ts +++ b/test/database/limit-database.test.ts @@ -1,5 +1,6 @@ import LimitDatabase from "@db/limit-database" import storage from "../__mock__/storage" +import { formatTimeYMD } from "@util/time" const db = new LimitDatabase(storage.local) @@ -45,6 +46,7 @@ describe('limit-database', () => { }) test("update waste", async () => { + const date = formatTimeYMD(new Date()) const id1 = await db.save({ cond: ["a.*.com"], time: 21, @@ -57,15 +59,15 @@ describe('limit-database', () => { enabled: true, allowDelay: false, }) - await db.updateWaste("20220606", { + await db.updateWaste(date, { [id1]: 10, // Not exist, no error throws - [Number.MAX_VALUE]: 20, + [-1]: 20, }) const all = await db.all() const used = all.find(a => a.cond?.includes("a.*.com")) - expect(used?.latestDate).toEqual("20220606") - expect(used?.wasteTime).toEqual(10) + expect(used?.records?.[date]).toBeTruthy() + expect(used?.records?.[date].mill).toEqual(10) }) test("import data", async () => { @@ -93,8 +95,7 @@ describe('limit-database', () => { const imported = await db.all() const cond2After = imported.find(a => a.cond?.includes("cond2")) - expect(!!cond2After?.latestDate).toBeFalsy() - expect(!!cond2After?.wasteTime).toBeFalsy() + expect(Object.values(cond2After?.records)).toBeTruthy() expect(cond2After?.allowDelay).toEqual(cond2.allowDelay) expect(cond2After?.enabled).toEqual(cond2.enabled) }) @@ -119,7 +120,7 @@ describe('limit-database', () => { const id = await db.save(data) await db.updateDelay(id, true) await db.updateDelay(Number.MAX_VALUE, true) - const all: timer.limit.Record[] = await db.all() + const all = await db.all() expect(all.length).toEqual(1) const item = all[0] expect(item.allowDelay).toBeTruthy() diff --git a/test/database/period-database.test.ts b/test/database/period-database.test.ts index 9ba55623..398ee89a 100644 --- a/test/database/period-database.test.ts +++ b/test/database/period-database.test.ts @@ -1,7 +1,6 @@ -import { DATE_FORMAT } from "@db/common/constant" import PeriodDatabase from "@db/period-database" import { keyOf, MILL_PER_PERIOD } from "@util/period" -import { formatTime } from "@util/time" +import { formatTimeYMD } from "@util/time" import storage from "../__mock__/storage" const db = new PeriodDatabase(storage.local) @@ -15,7 +14,7 @@ describe('period-database', () => { test('1', async () => { const date = new Date(2021, 5, 7) - const dateStr = formatTime(date, DATE_FORMAT) + const dateStr = formatTimeYMD(date) const yesterday = new Date(2021, 5, 6) expect((await db.get(dateStr))).toEqual({}) @@ -31,7 +30,7 @@ describe('period-database', () => { ]) const data = await db.get(dateStr) expect(data).toEqual({ 0: 56999, 1: 22 }) - const yesterdayStr = formatTime(yesterday, DATE_FORMAT) + const yesterdayStr = formatTimeYMD(yesterday) const yesterdayData = await db.get(yesterdayStr) expect(yesterdayData).toEqual({ 95: 2 }) }) diff --git a/test/database/stat-database.test.ts b/test/database/stat-database.test.ts index 917ae75c..ca31fb75 100644 --- a/test/database/stat-database.test.ts +++ b/test/database/stat-database.test.ts @@ -1,12 +1,11 @@ -import { DATE_FORMAT } from "@db/common/constant" import StatDatabase, { StatCondition } from "@db/stat-database" -import { formatTime, MILL_PER_DAY } from "@util/time" +import { formatTimeYMD, MILL_PER_DAY } from "@util/time" import { resultOf } from "@util/stat" import storage from "../__mock__/storage" const db = new StatDatabase(storage.local) const now = new Date() -const nowStr = formatTime(now, DATE_FORMAT) +const nowStr = formatTimeYMD(now) const yesterday = new Date(now.getTime() - MILL_PER_DAY) const beforeYesterday = new Date(now.getTime() - MILL_PER_DAY * 2) const baidu = 'www.baidu.com' @@ -163,7 +162,7 @@ describe('stat-database', () => { const data = await db.select({}) expect(data.length).toEqual(1) const item = data[0] - expect(item.date).toEqual(formatTime(now, "{y}{m}{d}")) + expect(item.date).toEqual(formatTimeYMD(now)) expect(item.host).toEqual(baidu) expect(item.focus).toEqual(1) expect(item.time).toEqual(1) diff --git a/test/util/time.test.ts b/test/util/time.test.ts index 901c57c2..0e533943 100644 --- a/test/util/time.test.ts +++ b/test/util/time.test.ts @@ -91,7 +91,7 @@ test("get week time", () => { // now 2022/05/22, Sun. const now = new Date(2022, 4, 22) // [2022/05/16, 2022/05/22] - let [s1, e1] = getWeekTime(now, true) + let [s1, e1] = getWeekTime(now, 'default', 'zh_CN') expect(s1.getDate()).toEqual(16) expect(s1.getHours()).toEqual(0) expect(s1.getMinutes()).toEqual(0) @@ -99,7 +99,7 @@ test("get week time", () => { expect(s1.getMilliseconds()).toEqual(0) expect(e1).toEqual(now) // [2022/05/22, 2022/05/28] - let [s2] = getWeekTime(now, false) + let [s2] = getWeekTime(now, 'default', 'en') expect(s2.getDate()).toEqual(22) }) diff --git a/types/timer/limit.d.ts b/types/timer/limit.d.ts index 7c3996b7..75293484 100644 --- a/types/timer/limit.d.ts +++ b/types/timer/limit.d.ts @@ -18,10 +18,16 @@ declare namespace timer.limit { waste: number /** * Number of delays today - * - * @since 2.6.7 */ delayCount: number + /** + * Waste this week, milliseconds + */ + weeklyWaste: number + /** + * Delay count of this week + */ + weeklyDelayCount: number } type Rule = { /** @@ -40,6 +46,12 @@ declare namespace timer.limit { * Time limit per day, seconds */ time: number + /** + * Time limit per week, seconds + * + * @since 2.4.1 + */ + weekly?: number /** * Time limit per visit, seconds * @@ -57,23 +69,6 @@ declare namespace timer.limit { allowDelay?: boolean periods?: Period[] } - type Record = Rule & { - /** - * The latest record date - */ - latestDate: string - /** - * Time wasted in the latest record date - */ - wasteTime: number - /** - * Click count of more time - */ - delay?: { - date: string - count: number - } - } /** * @since 1.9.0 */ diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts index e1b97958..2400902d 100644 --- a/types/timer/option.d.ts +++ b/types/timer/option.d.ts @@ -31,12 +31,6 @@ declare namespace timer.option { * @since 0.5.0 */ displaySiteName: boolean - /** - * The start of one week - * - * @since 1.2.5 - */ - weekStart: WeekStartOption /** * Whether to merge domain by default * @@ -120,6 +114,11 @@ declare namespace timer.option { * @since 0.7.0 */ countLocalFiles: boolean + /** + * The start of one week + * @since 2.4.1 + */ + weekStart?: WeekStartOption } type DailyLimitOption = {