Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/preset-env": "^7.26.9",
"@crowdin/crowdin-api-client": "^1.41.4",
"@crowdin/crowdin-api-client": "^1.42.0",
"@rsdoctor/webpack-plugin": "^1.0.1",
"@swc/core": "^1.11.13",
"@swc/core": "^1.11.20",
"@swc/jest": "^0.2.37",
"@types/chrome": "0.0.313",
"@types/chrome": "0.0.315",
"@types/decompress": "^4.2.7",
"@types/generate-json-webpack-plugin": "^0.3.7",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.14",
"@types/node": "^22.14.1",
"@types/punycode": "^2.1.4",
"@types/webpack": "^5.28.5",
"@vue/babel-plugin-jsx": "^1.4.0",
Expand All @@ -47,7 +47,7 @@
"copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"decompress": "^4.2.1",
"eslint": "^9.23.0",
"eslint": "^9.24.0",
"filemanager-webpack-plugin": "^8.0.0",
"generate-json-webpack-plugin": "^2.0.0",
"html-webpack-plugin": "^5.6.3",
Expand All @@ -58,23 +58,23 @@
"mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"postcss-rtlcss": "^5.6.0",
"puppeteer": "^24.4.0",
"sass": "^1.86.0",
"postcss-rtlcss": "^5.7.0",
"puppeteer": "^24.6.1",
"sass": "^1.86.3",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "5.8.2",
"typescript": "5.8.3",
"url-loader": "^4.1.1",
"web-ext": "^8.5.0",
"webpack": "^5.98.0",
"webpack": "^5.99.5",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^13.0.0",
"@vueuse/core": "^13.1.0",
"countup.js": "^2.8.0",
"echarts": "^5.6.0",
"element-plus": "2.9.7",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ export const initElementLocale = async (app: App) => {
app.use(ElementPlus, { locale: EL_LOCALE })
}

export const EL_DATE_FORMAT = t(calendarMessages, { key: msg => msg.dateFormat, param: { y: 'YYYY', m: 'MM', d: 'DD' } })
export const dateFormat = () => t(calendarMessages, { key: msg => msg.dateFormat, param: { y: 'YYYY', m: 'MM', d: 'DD' } })
2 changes: 1 addition & 1 deletion src/i18n/message/app/option-resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
"authInfo": "One token with at least gist permission is required"
},
"obsidian_local_rest_api": {
"endpointInfo": "Only HTTP is available because it is not possible to configure CORS for extensions pages"
"endpointInfo": "Only HTTP is available, as CORS cannot be configured for extension pages"
},
"web_dav": {}
},
Expand Down
4 changes: 2 additions & 2 deletions src/pages/app/components/Analysis/components/Trend/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { t } from "@app/locale"
import { EL_DATE_FORMAT } from "@i18n/element"
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"
Expand Down Expand Up @@ -41,7 +41,7 @@ const _default = defineComponent({
<ElDatePicker
modelValue={dateRange.value}
disabledDate={(date: Date) => date.getTime() > new Date().getTime()}
format={EL_DATE_FORMAT}
format={dateFormat()}
type="daterange"
shortcuts={SHORTCUTS}
rangeSeparator="-"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import I18nNode from "@app/components/common/I18nNode"
import { t } from "@app/locale"
import { EL_DATE_FORMAT } from "@i18n/element"
import { dateFormat as elDateFormat } from "@i18n/element"
import { type ElementDatePickerShortcut } from "@pages/element-ui/date"
import { getDatePickerIconSlots } from "@pages/element-ui/rtl"
import { formatTime, getBirthday, MILL_PER_DAY } from "@util/time"
Expand Down Expand Up @@ -54,7 +54,7 @@ const _default = defineComponent({
style={{ width: "250px" } satisfies StyleValue}
startPlaceholder={startPlaceholder}
endPlaceholder={endPlaceholder}
dateFormat={EL_DATE_FORMAT}
dateFormat={elDateFormat()}
type="daterange"
disabledDate={(date: Date) => date.getTime() > yesterday}
shortcuts={pickerShortcuts}
Expand Down
128 changes: 128 additions & 0 deletions src/pages/app/components/Report/ReportFilter/BatchDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { type I18nKey, t } from "@app/locale"
import StatDatabase from "@db/stat-database"
import { DeleteFilled } from "@element-plus/icons-vue"
import statService from "@service/stat-service"
import { groupBy, sum } from "@util/array"
import { formatTime } from "@util/time"
import { ElButton, ElMessage, ElMessageBox } from "element-plus"
import { defineComponent } from "vue"
import { useReportComponent, useReportFilter } from "../context"
import type { DisplayComponent, ReportFilterOption } from "../types"

const statDatabase = new StatDatabase(chrome.storage.local)

async function computeBatchDeleteMsg(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined): Promise<string> {
// host => total focus
const hostFocus: { [host: string]: number } = groupBy(selected,
a => a.siteKey?.host,
grouped => grouped.map(a => a.focus).reduce((a, b) => a + b, 0)
)
const hosts = Object.keys(hostFocus)
if (!hosts.length) {
// Never happen
return t(msg => msg.report.batchDelete.noSelectedMsg)
}
const count2Delete: number = mergeDate
// All the items
? sum(await Promise.all(Array.from(hosts).map(host => statService.count({ host, fullHost: true, date: dateRange }))))
// The count of row
: selected?.length || 0
const i18nParam: Record<string, string | number | undefined> = {
// count
count: count2Delete,
// example for hosts
example: hosts[0],
// Start date, if range
start: undefined,
// End date, if range
end: undefined,
// Date, if single date
date: undefined,
}

let key: I18nKey | undefined = undefined
const hasDateRange = dateRange?.length === 2 && (dateRange[0] || dateRange[1])
if (!hasDateRange) {
// Delete all
key = msg => msg.report.batchDelete.confirmMsgAll
} else {
const dateFormat = t(msg => msg.calendar.dateFormat)
const startDate = dateRange[0]
const endDate = dateRange[1]
const start = formatTime(startDate, dateFormat)
const end = formatTime(endDate, dateFormat)
if (start === end) {
// Single date
key = msg => msg.report.batchDelete.confirmMsg
i18nParam.date = start
} else {
// Date range
key = msg => msg.report.batchDelete.confirmMsgRange
i18nParam.start = start
i18nParam.end = end
}
}
return t(key, i18nParam)
}

async function handleBatchDelete(displayComp: DisplayComponent | undefined, filter: ReportFilterOption) {
if (!displayComp) return

const selected: timer.stat.Row[] = displayComp?.getSelected?.() || []
if (!selected?.length) {
ElMessage.info(t(msg => msg.report.batchDelete.noSelectedMsg))
return
}
const { dateRange, mergeDate } = filter
ElMessageBox({
message: await computeBatchDeleteMsg(selected, mergeDate, dateRange),
type: "warning",
confirmButtonText: t(msg => msg.button.okey),
showCancelButton: true,
cancelButtonText: t(msg => msg.button.dont),
// Cant close this on press ESC
closeOnPressEscape: false,
// Cant close this on clicking modal
closeOnClickModal: false
}).then(async () => {
// Delete
await deleteBatch(selected, mergeDate, dateRange)
ElMessage.success(t(msg => msg.operation.successMsg))
displayComp?.refresh?.()
}).catch(() => {
// Do nothing
})
}

async function deleteBatch(selected: timer.stat.Row[], mergeDate: boolean, dateRange: [Date, Date] | undefined) {
if (mergeDate) {
// Delete according to the date range
const start = dateRange?.[0]
const end = dateRange?.[1]
const hosts = selected.map(d => d.siteKey?.host)
await Promise.all(hosts.map(async h => h && await statDatabase.deleteByUrlBetween(h, start, end)))
} else {
// If not merge date, batch delete
await statService.batchDelete(selected)
}
}

const BatchDelete = defineComponent(() => {
const filter = useReportFilter()
const comp = useReportComponent()

return () => (
<ElButton
v-show={!filter.readRemote}
disabled={!!filter.siteMerge}
type="primary"
link
icon={<DeleteFilled />}
onClick={() => handleBatchDelete(comp.value, filter)}
>
{t(msg => msg.button.batchDelete)}
</ElButton>
)
})

export default BatchDelete
68 changes: 39 additions & 29 deletions src/pages/app/components/Report/ReportFilter/DownloadFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,51 @@
* https://opensource.org/licenses/MIT
*/

import { useCategories } from "@app/context"
import { Download } from "@element-plus/icons-vue"
import statService from "@service/stat-service"
import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus"
import { defineComponent } from "vue"
import type { FileFormat } from "../types"
import { cvtOption2Param } from "../common"
import { useReportFilter } from "../context"
import { exportCsv, exportJson } from "../file-export"
import { ICON_BTN_STYLE } from "./common"

const ALL_FILE_FORMATS: FileFormat[] = ["json", "csv"]
const ALL_FILE_FORMATS = ["json", "csv"] as const
type FileFormat = typeof ALL_FILE_FORMATS[number]

const _default = defineComponent({
emits: {
download: (_format: FileFormat) => true,
},
setup(_, ctx) {
const handleClick = (format: FileFormat) => ctx.emit('download', format)
return () => (
<ElDropdown
showTimeout={100}
v-slots={{
dropdown: () => <ElDropdownMenu>
{ALL_FILE_FORMATS.map(f =>
<ElDropdownItem onClick={() => handleClick(f)}>
{f}
</ElDropdownItem>
)}
</ElDropdownMenu>
}}
>
<ElButton size="small" style={ICON_BTN_STYLE}>
<ElIcon size={17} style={{ padding: "0 1px" }}>
<Download />
</ElIcon>
</ElButton>
</ElDropdown>
)
const DownloadFile = defineComponent(() => {
const filter = useReportFilter()
const { categories } = useCategories()

const handleDownload = async (format: FileFormat) => {
const categoriesVal = categories.value
const param = cvtOption2Param(filter)
const rows = await statService.select(param, true)
format === 'json' && exportJson(filter, rows, categoriesVal)
format === 'csv' && exportCsv(filter, rows, categoriesVal)
}

return () => (
<ElDropdown
showTimeout={100}
v-slots={{
dropdown: () => <ElDropdownMenu>
{ALL_FILE_FORMATS.map(f =>
<ElDropdownItem onClick={() => handleDownload(f)}>
{f}
</ElDropdownItem>
)}
</ElDropdownMenu>
}}
>
<ElButton size="small" style={ICON_BTN_STYLE}>
<ElIcon size={17} style={{ padding: "0 1px" }}>
<Download />
</ElIcon>
</ElButton>
</ElDropdown>
)
})

export default _default
export default DownloadFile
Loading
Loading