From 47c45f95efd4dbc790f981c85a9a39a984c62de6 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 18 Sep 2024 00:00:00 +0800 Subject: [PATCH 1/9] Semi --- src/api/quantified-resume.ts | 49 +++++++++++ .../Option/components/BackupOption/index.tsx | 21 ++++- src/common/backup/processor.ts | 71 ++++++++-------- .../backup/quantified-resume/coordinator.ts | 84 +++++++++++++++++++ src/common/backup/web-dav/coordinator.ts | 2 +- src/i18n/message/app/option.ts | 11 +-- types/timer/backup.d.ts | 5 +- 7 files changed, 199 insertions(+), 44 deletions(-) create mode 100644 src/api/quantified-resume.ts create mode 100644 src/common/backup/quantified-resume/coordinator.ts diff --git a/src/api/quantified-resume.ts b/src/api/quantified-resume.ts new file mode 100644 index 000000000..43ad577c9 --- /dev/null +++ b/src/api/quantified-resume.ts @@ -0,0 +1,49 @@ +import { fetchGet, fetchPost } from "./http" + +const QR_BUILTIN_TYPE = "BrowserTime" +export const DEFAULT_ENDPOINT = "http://localhost:12233" + +export type BucketPayload = Pick + +export type Bucket = { + id?: number + no?: number + name: string + builtin: "BrowserTime" + builtinRefId: string + status?: "Enabled" | "Disabled" + desc?: string + created?: number + lastModified?: number + payload?: BucketPayload +} + +export type QrRequestContext = { + endpoint?: string +} + +export const listBuckets = async (ctx: QrRequestContext, clientId?: string): Promise => { + const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket?bt=${QR_BUILTIN_TYPE}&bt_rid=${clientId || ''}`) + const status = response?.status + if (status === 200) { + return await response.json() + } else if (status === 422) { + throw new Error("Failed to connect Quantified Resume, please contact the developer") + } else { + console.error(response) + throw new Error("Unexpected status code: " + status) + } +} + +export const createBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { + const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket) + const status = response?.status + if (status === 200) { + return await response.json() + } else if (status === 422) { + throw new Error("Failed to connect Quantified Resume, please contact the developer") + } else { + console.error(response) + throw new Error("Unexpected status code: " + status) + } +} \ No newline at end of file diff --git a/src/app/components/Option/components/BackupOption/index.tsx b/src/app/components/Option/components/BackupOption/index.tsx index 3d73c59c8..c24c94229 100644 --- a/src/app/components/Option/components/BackupOption/index.tsx +++ b/src/app/components/Option/components/BackupOption/index.tsx @@ -8,6 +8,9 @@ import { DEFAULT_VAULT as DEFAULT_OBSIDIAN_BUCKET, DEFAULT_ENDPOINT as DEFAULT_OBSIDIAN_ENDPOINT, } from "@api/obsidian" +import { + DEFAULT_ENDPOINT as DEFAULT_QR_ENDPOINT, +} from "@api/quantified-resume" import { t } from "@app/locale" import { AUTHOR_EMAIL } from "@src/package" import { ElAlert, ElInput, ElOption, ElSelect } from "element-plus" @@ -25,13 +28,15 @@ const ALL_TYPES: timer.backup.Type[] = [ 'gist', 'web_dav', 'obsidian_local_rest_api', + 'quantified_resume', ] const TYPE_NAMES: { [t in timer.backup.Type]: string } = { none: t(msg => msg.option.backup.meta.none.label), gist: 'GitHub Gist', obsidian_local_rest_api: 'Obsidian - Local REST API', - web_dav: 'WebDAV' + web_dav: 'WebDAV', + quantified_resume: 'Quantified Resume', } const _default = defineComponent((_props, ctx) => { @@ -172,6 +177,20 @@ const _default = defineComponent((_props, ctx) => { /> } + {backupType.value === 'quantified_resume' && <> + msg.option.backup.label.endpoint} + v-slots={{ info: () => '' }} + > + setExtField('endpoint', val)} + /> + + } msg.option.backup.client}> coordinator: timer.backup.Coordinator errorMsg?: string } +function prepareAuth(option: timer.option.BackupOption): timer.backup.Auth { + const type = option?.backupType || 'none' + const token = option?.backupAuths?.[type] + const login = option.backupLogin?.[type] + return { token, login } +} + class CoordinatorContextWrapper implements timer.backup.CoordinatorContext { auth: timer.backup.Auth ext?: timer.backup.TypeExt cache: Cache type: timer.backup.Type cid: string + cname: string + + constructor(cid: string, option: timer.option.BackupOption) { + const { backupType, backupExts, clientName } = option || {} + this.type = backupType || 'none' + this.ext = backupExts?.[this.type] + this.auth = prepareAuth(option) - constructor(cid: string, auth: timer.backup.Auth, ext: timer.backup.TypeExt, type: timer.backup.Type) { this.cid = cid - this.auth = auth - this.ext = ext - this.type = type + this.cname = clientName } async init(): Promise> { @@ -133,13 +143,6 @@ function filterClient(c: timer.backup.Client, excludeLocal: boolean, localClient return true } -function prepareAuth(option: timer.option.BackupOption): timer.backup.Auth { - const type = option?.backupType || 'none' - const token = option?.backupAuths?.[type] - const login = option.backupLogin?.[type] - return { token, login } -} - export type RemoteQueryParam = { start: Date end: Date @@ -158,18 +161,17 @@ class Processor { gist: new GistCoordinator(), obsidian_local_rest_api: new ObsidianCoordinator(), web_dav: new WebDAVCoordinator(), + quantified_resume: new QuantifiedResumeCoordinator(), } } async syncData(): Promise> { - const { option, auth, ext, type, coordinator, errorMsg } = await this.checkAuth() + const { type, context, coordinator, errorMsg } = await this.checkAuth() if (errorMsg) return error(errorMsg) - - const cid = await lazyGetCid() - const context: timer.backup.CoordinatorContext = await new CoordinatorContextWrapper(cid, auth, ext, type).init() + let cid = context.cid const client: timer.backup.Client = { id: cid, - name: option.clientName, + name: context.cname, minDate: undefined, maxDate: undefined } @@ -191,49 +193,47 @@ class Processor { } async listClients(): Promise> { - const { auth, ext, type, coordinator, errorMsg } = await this.checkAuth() + const { context, coordinator, errorMsg } = await this.checkAuth() if (errorMsg) return error(errorMsg) - const cid = await lazyGetCid() - const context: timer.backup.CoordinatorContext = await new CoordinatorContextWrapper(cid, auth, ext, type).init() + await context.init() const clients = await coordinator.listAllClients(context) return success(clients) } async checkAuth(): Promise { const option = (await optionService.getAllOption()) as timer.option.BackupOption - const type = option?.backupType || 'none' - const ext = option?.backupExts?.[type] - const auth = prepareAuth(option) + const type = option.backupType + const cid = await lazyGetCid() + const context = new CoordinatorContextWrapper(cid, option) - const coordinator: timer.backup.Coordinator = type && this.coordinators[type] + const coordinator: timer.backup.Coordinator = this.coordinators?.[context.type] if (!coordinator) { // no coordinator, do nothing - return { option, auth, ext, type, coordinator, errorMsg: "Invalid type" } + return { type, context, coordinator, errorMsg: "Invalid type" } } let errorMsg: string try { - errorMsg = await coordinator.testAuth(auth, ext) + errorMsg = await coordinator.testAuth(context.auth, context.ext) } catch (e) { errorMsg = (e as Error)?.message || 'Unknown error' } - return { option, auth, ext, type, coordinator, errorMsg } + return { type, context, coordinator, errorMsg } } async query(param: RemoteQueryParam): Promise { - const { type, coordinator, auth, ext, errorMsg } = await this.checkAuth() + const { coordinator, context, errorMsg } = await this.checkAuth() if (errorMsg || !coordinator) { return [] } const { start = getBirthday(), end, specCid, excludeLocal } = param - let localCid = await metaService.getCid() // 1. init context - const context: timer.backup.CoordinatorContext = await new CoordinatorContextWrapper(localCid, auth, ext, type).init() + await context.init() // 2. query all clients, and filter them 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 => filterClient(c, excludeLocal, context.cid, startStr, endStr)) .filter(c => !specCid || c.id === specCid) // 3. iterate clients const result: timer.stat.Row[] = [] @@ -255,10 +255,9 @@ class Processor { } async clear(cid: string): Promise> { - const { auth, ext, type, coordinator, errorMsg } = await this.checkAuth() + const { context, coordinator, errorMsg } = await this.checkAuth() if (errorMsg) return error(errorMsg) - let localCid = await metaService.getCid() - const context: timer.backup.CoordinatorContext = await new CoordinatorContextWrapper(localCid, auth, ext, type).init() + await context.init() // 1. Find the client const allClients = await coordinator.listAllClients(context) const client = allClients?.filter(c => c?.id === cid)?.[0] diff --git a/src/common/backup/quantified-resume/coordinator.ts b/src/common/backup/quantified-resume/coordinator.ts new file mode 100644 index 000000000..8078b650e --- /dev/null +++ b/src/common/backup/quantified-resume/coordinator.ts @@ -0,0 +1,84 @@ +import { Bucket, listBuckets } from "@api/quantified-resume" + +export type QuantifiedResumeCache = { + bucketIds: { + // clientId => bucketId + [clientId: string]: number + } +} + +async function getBucketId(context: timer.backup.CoordinatorContext): Promise { + const { cid, cache } = context || {} + // 1. query from cache + let bucketId = cache?.bucketIds?.[cid] + if (!bucketId) return bucketId + + const { endpoint } = context?.ext || {} + // 2. query again + bucketId = (await listBuckets({ endpoint }, cid))?.[0]?.id + // TODO + if (!bucketId) { + // 3. create one + const bucket: Bucket = { + name: "Time Tracker: " + cid, + builtin: "BrowserTime", + builtinRefId: cid, + payload: { + name: "" + } + } + } + throw new Error("TODO") +} + +export default class QuantifiedResumeCoordinator implements timer.backup.Coordinator { + + updateClients(_: timer.backup.CoordinatorContext, clients: timer.backup.Client[]): Promise { + throw new Error("Method not implemented."); + } + + async listAllClients(context: timer.backup.CoordinatorContext): Promise { + const { endpoint } = context?.ext || {} + const buckets = await listBuckets({ endpoint }) + + let result: timer.backup.Client[] = [] + let bucketIds: { [clientId: string]: number } = {} + buckets?.forEach(({ payload, id: bucketId, builtinRefId, name: bucketName }) => { + let { name, minDate, maxDate } = payload || {} + const client: timer.backup.Client = { + id: builtinRefId, + name: name || bucketName, + minDate, maxDate, + } + result.push(client) + bucketIds[builtinRefId] = bucketId + }) + + context.cache = { ...(context.cache || {}), bucketIds } + await context.handleCacheChanged() + + return result + } + + download(context: timer.backup.CoordinatorContext, dateStart: Date, dateEnd: Date, targetCid?: string): Promise { + throw new Error("Method not implemented."); + } + + async upload(context: timer.backup.CoordinatorContext, rows: timer.stat.RowBase[]): Promise { + const bucketId = await getBucketId(context) + throw new Error("Method not implemented."); + } + + async testAuth(_auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise { + try { + const { endpoint } = ext || {} + await listBuckets({ endpoint }) + } catch (e) { + return (e as Error)?.message || e || 'Unknown error' + } + } + + clear(context: timer.backup.CoordinatorContext, client: timer.backup.Client): Promise { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/common/backup/web-dav/coordinator.ts b/src/common/backup/web-dav/coordinator.ts index f85ad316d..bd127748e 100644 --- a/src/common/backup/web-dav/coordinator.ts +++ b/src/common/backup/web-dav/coordinator.ts @@ -101,7 +101,7 @@ export default class WebDAVCoordinator implements timer.backup.Coordinator]: { + gist: { + authInfo: string + } + obsidian_local_rest_api: { endpointInfo: string } } diff --git a/types/timer/backup.d.ts b/types/timer/backup.d.ts index 66e3b4e8a..82d3ab7ee 100644 --- a/types/timer/backup.d.ts +++ b/types/timer/backup.d.ts @@ -22,8 +22,9 @@ declare namespace timer.backup { interface CoordinatorContext { cid: string + cname: string + type: timer.backup.Type auth?: Auth - login?: LoginInfo ext?: TypeExt cache: Cache handleCacheChanged: () => Promise @@ -72,6 +73,8 @@ declare namespace timer.backup { | 'obsidian_local_rest_api' // @since 2.4.5 | 'web_dav' + // @since 3.0.0 + | 'quantified_resume' type AuthType = | 'token' From d5a24cefb9b72d85b707a8fbcccc9fc96511b3b0 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Wed, 18 Sep 2024 19:57:47 +0800 Subject: [PATCH 2/9] Semi --- src/api/http.ts | 14 +++++ src/api/quantified-resume.ts | 24 ++++++++- .../backup/quantified-resume/coordinator.ts | 54 +++++++++++++------ 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/api/http.ts b/src/api/http.ts index 89a9822ca..934c4b483 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -32,6 +32,20 @@ export async function fetchPost(url: string, body?: T, option?: Option): Prom } } +export async function fetchPut(url: string, body?: T, option?: Option): Promise { + try { + const response = await fetch(url, { + ...(option || {}), + method: "PUT", + body: body ? JSON.stringify(body) : null, + }) + return response + } catch (e) { + console.error("Failed to fetch post", e) + throw Error(e) + } +} + export async function fetchPutText(url: string, bodyText?: string, option?: Option): Promise { try { const response = await fetch(url, { diff --git a/src/api/quantified-resume.ts b/src/api/quantified-resume.ts index 43ad577c9..aa23eb50c 100644 --- a/src/api/quantified-resume.ts +++ b/src/api/quantified-resume.ts @@ -1,4 +1,4 @@ -import { fetchGet, fetchPost } from "./http" +import { fetchGet, fetchPost, fetchPut } from "./http" const QR_BUILTIN_TYPE = "BrowserTime" export const DEFAULT_ENDPOINT = "http://localhost:12233" @@ -22,6 +22,12 @@ export type QrRequestContext = { endpoint?: string } +const headers = (): Headers => { + const headers = new Headers() + headers.append("Content-Type", "application/json") + return headers +} + export const listBuckets = async (ctx: QrRequestContext, clientId?: string): Promise => { const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket?bt=${QR_BUILTIN_TYPE}&bt_rid=${clientId || ''}`) const status = response?.status @@ -36,7 +42,7 @@ export const listBuckets = async (ctx: QrRequestContext, clientId?: string): Pro } export const createBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { - const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket) + const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket, { headers: headers() }) const status = response?.status if (status === 200) { return await response.json() @@ -46,4 +52,18 @@ export const createBucket = async (ctx: QrRequestContext, bucket: Bucket): Promi console.error(response) throw new Error("Unexpected status code: " + status) } +} + +export const updateBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { + const response = await fetchPut(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucket?.id}`, bucket, { headers: headers() }) + const status = response?.status + if (status === 200) { + return + } + if (status === 422) { + throw new Error("Failed to connect Quantified Resume, please contact the developer") + } else { + console.error(response) + throw new Error("Unexpected status code: " + status) + } } \ No newline at end of file diff --git a/src/common/backup/quantified-resume/coordinator.ts b/src/common/backup/quantified-resume/coordinator.ts index 8078b650e..380e219af 100644 --- a/src/common/backup/quantified-resume/coordinator.ts +++ b/src/common/backup/quantified-resume/coordinator.ts @@ -1,4 +1,7 @@ -import { Bucket, listBuckets } from "@api/quantified-resume" +import { Bucket, createBucket, listBuckets, updateBucket } from "@api/quantified-resume" +import metaMessages, { } from "@i18n/message/common/meta" +import { t } from "@i18n" +import { groupBy } from "@util/array" export type QuantifiedResumeCache = { bucketIds: { @@ -7,6 +10,19 @@ export type QuantifiedResumeCache = { } } +async function createNewBucket(context: timer.backup.CoordinatorContext): Promise { + const { cid, cname } = context || {} + const { endpoint } = context?.ext || {} + const appName = t(metaMessages, { key: msg => msg.name }) + const bucket: Bucket = { + name: `${appName}: ${cid}`, + builtin: "BrowserTime", + builtinRefId: cid, + payload: { name: cname } + } + return createBucket({ endpoint }, bucket) +} + async function getBucketId(context: timer.backup.CoordinatorContext): Promise { const { cid, cache } = context || {} // 1. query from cache @@ -16,25 +32,31 @@ async function getBucketId(context: timer.backup.CoordinatorContext { - - updateClients(_: timer.backup.CoordinatorContext, clients: timer.backup.Client[]): Promise { - throw new Error("Method not implemented."); + async updateClients(context: timer.backup.CoordinatorContext, clients: timer.backup.Client[]): Promise { + const { endpoint } = context?.ext || {} + const existBuckets = groupBy(await listBuckets({ endpoint }) || [], b => b.builtinRefId, l => l?.[0]) + if (!clients?.length) return + const promises = Promise.all(clients.map( + async ({ id, name, minDate, maxDate }) => { + const exist = existBuckets[id] + if (exist) { + // update payload + exist.payload = { name, minDate, maxDate } + await updateBucket({ endpoint }, exist) + } else { + await createNewBucket(context) + } + }) + ) + await promises } async listAllClients(context: timer.backup.CoordinatorContext): Promise { @@ -66,7 +88,9 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin async upload(context: timer.backup.CoordinatorContext, rows: timer.stat.RowBase[]): Promise { const bucketId = await getBucketId(context) - throw new Error("Method not implemented."); + rows.forEach(row => { + + }) } async testAuth(_auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise { From 252405510f45b7c93022af92f40b428e05b7cd42 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Fri, 20 Sep 2024 10:10:27 +0800 Subject: [PATCH 3/9] Semi --- src/api/quantified-resume.ts | 73 +++++++++++++------ .../backup/quantified-resume/coordinator.ts | 55 +++++++++++--- src/util/array.ts | 6 +- 3 files changed, 98 insertions(+), 36 deletions(-) diff --git a/src/api/quantified-resume.ts b/src/api/quantified-resume.ts index aa23eb50c..6d7b1ba1b 100644 --- a/src/api/quantified-resume.ts +++ b/src/api/quantified-resume.ts @@ -1,4 +1,4 @@ -import { fetchGet, fetchPost, fetchPut } from "./http" +import { fetchDelete, fetchGet, fetchPost, fetchPut } from "./http" const QR_BUILTIN_TYPE = "BrowserTime" export const DEFAULT_ENDPOINT = "http://localhost:12233" @@ -18,6 +18,18 @@ export type Bucket = { payload?: BucketPayload } +export type Item = { + refId: string + timestamp: number + name?: string + action: string + metrics: { + visit: number + focus: number + } + payload?: Record +} + export type QrRequestContext = { endpoint?: string } @@ -30,40 +42,57 @@ const headers = (): Headers => { export const listBuckets = async (ctx: QrRequestContext, clientId?: string): Promise => { const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket?bt=${QR_BUILTIN_TYPE}&bt_rid=${clientId || ''}`) - const status = response?.status - if (status === 200) { - return await response.json() - } else if (status === 422) { - throw new Error("Failed to connect Quantified Resume, please contact the developer") - } else { - console.error(response) - throw new Error("Unexpected status code: " + status) - } + return handleResponseJson(response) } export const createBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket`, bucket, { headers: headers() }) - const status = response?.status - if (status === 200) { - return await response.json() - } else if (status === 422) { - throw new Error("Failed to connect Quantified Resume, please contact the developer") - } else { - console.error(response) - throw new Error("Unexpected status code: " + status) - } + return handleResponseJson(response) } -export const updateBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { - const response = await fetchPut(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucket?.id}`, bucket, { headers: headers() }) +async function handleResponseJson(response: Response): Promise { + await handleResponse(response) + return response.json() +} + +async function handleResponse(response: Response): Promise { const status = response?.status if (status === 200) { return } + let resMsg = null + try { + resMsg = (await response.json()).message + } catch { } + if (resMsg) { + throw new Error(resMsg) + } if (status === 422) { throw new Error("Failed to connect Quantified Resume, please contact the developer") + } else if (status === 500) { + throw new Error("Internal server error") } else { console.error(response) - throw new Error("Unexpected status code: " + status) + throw new Error(`Unexpected status code: ${status}, url=${response.url}`) } +} + +export const updateBucket = async (ctx: QrRequestContext, bucket: Bucket): Promise => { + const response = await fetchPut(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucket?.id}`, bucket, { headers: headers() }) + await handleResponse(response) +} + +export const batchCreateItems = async (ctx: QrRequestContext, bucketId: number, items: Item[]): Promise => { + const response = await fetchPost(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}/item`, items, { headers: headers() }) + await handleResponse(response) +} + +export const listAllItems = async (ctx: QrRequestContext, bucketId: number): Promise => { + const response = await fetchGet(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}/item`, { headers: headers() }) + return await handleResponseJson(response) +} + +export const removeBucket = async (ctx: QrRequestContext, bucketId: number): Promise => { + const response = await fetchDelete(`${ctx?.endpoint || DEFAULT_ENDPOINT}/api/0/bucket/${bucketId}?force=true`, { headers: headers() }) + await handleResponse(response) } \ No newline at end of file diff --git a/src/common/backup/quantified-resume/coordinator.ts b/src/common/backup/quantified-resume/coordinator.ts index 380e219af..dcc20ee8d 100644 --- a/src/common/backup/quantified-resume/coordinator.ts +++ b/src/common/backup/quantified-resume/coordinator.ts @@ -1,7 +1,8 @@ -import { Bucket, createBucket, listBuckets, updateBucket } from "@api/quantified-resume" +import { batchCreateItems, Bucket, createBucket, Item, listAllItems, listBuckets, removeBucket, updateBucket } from "@api/quantified-resume" import metaMessages, { } from "@i18n/message/common/meta" import { t } from "@i18n" import { groupBy } from "@util/array" +import { formatTimeYMD, parseTime } from "@util/time" export type QuantifiedResumeCache = { bucketIds: { @@ -23,15 +24,16 @@ async function createNewBucket(context: timer.backup.CoordinatorContext): Promise { - const { cid, cache } = context || {} +async function getBucketId(context: timer.backup.CoordinatorContext, specificCid?: string): Promise { + const cid = specificCid || context?.cid + const { cache } = context || {} // 1. query from cache let bucketId = cache?.bucketIds?.[cid] - if (!bucketId) return bucketId + if (bucketId) return bucketId const { endpoint } = context?.ext || {} // 2. query again - bucketId = (await listBuckets({ endpoint }, cid))?.[0]?.id + bucketId = (await listBuckets({ endpoint }, cid))?.filter(b => b.builtinRefId === cid)?.[0]?.id if (!bucketId) { // 3. create one bucketId = await createNewBucket(context) @@ -82,15 +84,44 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin return result } - download(context: timer.backup.CoordinatorContext, dateStart: Date, dateEnd: Date, targetCid?: string): Promise { - throw new Error("Method not implemented."); + async download(context: timer.backup.CoordinatorContext, dateStart: Date, dateEnd: Date, targetCid?: string): Promise { + let bucketId = await getBucketId(context, targetCid) + if (!bucketId) return [] + const items = await listAllItems({ endpoint: context?.ext?.endpoint }, bucketId) + return items?.map(({ name, timestamp, metrics }) => ({ + host: name, + date: formatTimeYMD(timestamp), + focus: metrics?.focus, + time: metrics?.visit, + } satisfies timer.stat.RowBase)) || [] } async upload(context: timer.backup.CoordinatorContext, rows: timer.stat.RowBase[]): Promise { - const bucketId = await getBucketId(context) - rows.forEach(row => { + if (!rows?.length) return + const bucketId = await getBucketId(context) + let items = rows.map(({ host, date, focus, time: visit }) => { + const time = parseTime(date) + time.setHours(0) + time.setMinutes(0) + time.setSeconds(0) + time.setMilliseconds(0) + const item: Item = { + refId: `${date}${host}`, + timestamp: time.getTime(), + metrics: { visit, focus }, + action: "web_time", + name: host, + payload: { date, host, cid: context.cid }, + } + return item }) + const groups = groupBy(items, (_, idx) => Math.floor(idx / 2000), l => l) + + const { endpoint } = context?.ext || {} + for (const group of Object.values(groups)) { + await batchCreateItems({ endpoint }, bucketId, group) + } } async testAuth(_auth: timer.backup.Auth, ext: timer.backup.TypeExt): Promise { @@ -102,7 +133,9 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin } } - clear(context: timer.backup.CoordinatorContext, client: timer.backup.Client): Promise { - throw new Error("Method not implemented."); + async clear(context: timer.backup.CoordinatorContext, client: timer.backup.Client): Promise { + const bucketId = await getBucketId(context, client.id) + if (!bucketId) return + await removeBucket({ endpoint: context?.ext?.endpoint }, bucketId) } } \ No newline at end of file diff --git a/src/util/array.ts b/src/util/array.ts index 6ed9d1c7a..076677d5f 100644 --- a/src/util/array.ts +++ b/src/util/array.ts @@ -15,12 +15,12 @@ */ export function groupBy( arr: T[], - keyFunc: (e: T) => string | number, + keyFunc: (e: T, idx: number) => string | number, downstream: (grouped: T[], key: string) => R ): { [key: string]: R } { const groupedMap: { [key: string]: T[] } = {} - arr.forEach(e => { - const key = keyFunc(e) + arr.forEach((e, idx) => { + const key = keyFunc(e, idx) if (key === undefined || key === null) { return } From 18d25399fb415144128903332007eb9406470968 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 22 Sep 2024 14:43:46 +0800 Subject: [PATCH 4/9] Semi --- src/common/backup/quantified-resume/coordinator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/backup/quantified-resume/coordinator.ts b/src/common/backup/quantified-resume/coordinator.ts index dcc20ee8d..23f48d02e 100644 --- a/src/common/backup/quantified-resume/coordinator.ts +++ b/src/common/backup/quantified-resume/coordinator.ts @@ -1,6 +1,6 @@ import { batchCreateItems, Bucket, createBucket, Item, listAllItems, listBuckets, removeBucket, updateBucket } from "@api/quantified-resume" -import metaMessages, { } from "@i18n/message/common/meta" import { t } from "@i18n" +import metaMessages from "@i18n/message/common/meta" import { groupBy } from "@util/array" import { formatTimeYMD, parseTime } from "@util/time" From 8e115a97a2e140d37af6b65aa2f2a4a97811d2f4 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 28 Sep 2024 16:03:24 +0800 Subject: [PATCH 5/9] Support all time data on the popup page (#319) --- .../Dashboard/components/Calendar/Wrapper.ts | 6 +- .../Option/components/PopupOption.tsx | 2 +- .../message/common/calendar-resource.json | 6 +- src/i18n/message/common/calendar.ts | 1 + src/i18n/message/popup/chart-resource.json | 6 +- src/i18n/message/popup/chart.ts | 2 +- src/i18n/message/popup/index.ts | 3 + src/popup/common.ts | 18 ++++- src/popup/components/chart/click-handler.ts | 5 +- src/popup/components/chart/index.ts | 14 ++-- src/popup/components/chart/option.ts | 68 +++++++++++++------ src/popup/components/footer/index.ts | 46 ++++++++----- .../components/footer/select/time-select.ts | 13 ++-- src/popup/popup.d.ts | 13 ---- src/popup/style/index.sass | 2 +- src/service/stat-service/merge.ts | 14 ++-- src/util/constant/popup.ts | 4 +- types/timer/option.d.ts | 1 + types/timer/stat.d.ts | 6 ++ 19 files changed, 146 insertions(+), 84 deletions(-) delete mode 100644 src/popup/popup.d.ts diff --git a/src/app/components/Dashboard/components/Calendar/Wrapper.ts b/src/app/components/Dashboard/components/Calendar/Wrapper.ts index d798c6a77..e8ce1b7d7 100644 --- a/src/app/components/Dashboard/components/Calendar/Wrapper.ts +++ b/src/app/components/Dashboard/components/Calendar/Wrapper.ts @@ -20,6 +20,7 @@ import { REPORT_ROUTE } from "@app/router/constants" import { createTabAfterCurrent } from "@api/chrome/tab" import { getStepColors } from "@app/util/echarts" import { locale } from "@i18n" +import { cvt2LocaleTime } from "@app/util/time" type _Value = [ x: number, @@ -42,10 +43,7 @@ export type BizOption = { } function formatTooltip(mills: number, date: string): string { - const y = date.substring(0, 4) - const m = date.substring(4, 6) - const d = date.substring(6, 8) - const dateStr = t(msg => msg.calendar.dateFormat, { y, m, d }) + const dateStr = cvt2LocaleTime(date) const timeStr = formatPeriodCommon(mills) return `${dateStr}
${timeStr}` } diff --git a/src/app/components/Option/components/PopupOption.tsx b/src/app/components/Option/components/PopupOption.tsx index 131b9eb43..0311d4207 100644 --- a/src/app/components/Option/components/PopupOption.tsx +++ b/src/app/components/Option/components/PopupOption.tsx @@ -90,7 +90,7 @@ const _default = defineComponent((_props, ctx) => { modelValue={option.defaultDuration} size="small" style={{ width: `${tStyle(m => m.durationSelectWidth)}px` }} - onChange={(val: PopupDuration) => option.defaultDuration = val} + onChange={(val: timer.option.PopupDuration) => option.defaultDuration = val} > {ALL_POPUP_DURATION.map(item => )} diff --git a/src/i18n/message/common/calendar-resource.json b/src/i18n/message/common/calendar-resource.json index 42dd54eb6..9bc043f85 100644 --- a/src/i18n/message/common/calendar-resource.json +++ b/src/i18n/message/common/calendar-resource.json @@ -17,7 +17,8 @@ "thisMonth": "本月", "lastDays": "最近 {n} 天", "tillYesterday": "直到昨天", - "tillDaysAgo": "直到 {n} 天前" + "tillDaysAgo": "直到 {n} 天前", + "allTime": "所有时间" } }, "zh_TW": { @@ -59,7 +60,8 @@ "thisMonth": "This month", "lastDays": "Last {n} days", "tillYesterday": "Until yesterday", - "tillDaysAgo": "Until {n} days ago" + "tillDaysAgo": "Until {n} days ago", + "allTime": "All time" } }, "ja": { diff --git a/src/i18n/message/common/calendar.ts b/src/i18n/message/common/calendar.ts index ea03b3b10..f959ba422 100644 --- a/src/i18n/message/common/calendar.ts +++ b/src/i18n/message/common/calendar.ts @@ -26,6 +26,7 @@ export type CalendarMessage = { lastDays: string tillYesterday: string tillDaysAgo: string + allTime: string } } diff --git a/src/i18n/message/popup/chart-resource.json b/src/i18n/message/popup/chart-resource.json index 2527e7624..a72eb857c 100644 --- a/src/i18n/message/popup/chart-resource.json +++ b/src/i18n/message/popup/chart-resource.json @@ -4,7 +4,8 @@ "today": "今日数据", "thisWeek": "本周数据", "thisMonth": "本月数据", - "last30Days": "近 30 天数据" + "last30Days": "近 30 天数据", + "allTime": "全部数据" }, "mergeHostLabel": "合并子域名", "fileName": "上网时长清单_{today}_by_{app}", @@ -38,7 +39,8 @@ "today": "Today's Data", "thisWeek": "This Week's Data", "thisMonth": "This Month's Data", - "last30Days": "Last 30 days' data" + "last30Days": "Last 30 days' data", + "allTime": "All data" }, "mergeHostLabel": "Merge Sites", "fileName": "Web_Time_List_{today}_By_{app}", diff --git a/src/i18n/message/popup/chart.ts b/src/i18n/message/popup/chart.ts index e0eb8d491..1ffe39007 100644 --- a/src/i18n/message/popup/chart.ts +++ b/src/i18n/message/popup/chart.ts @@ -8,7 +8,7 @@ import resource from './chart-resource.json' export type ChartMessage = { - title: { [key in PopupDuration]: string } + title: { [key in timer.option.PopupDuration]: string } mergeHostLabel: string fileName: string saveAsImageTitle: string diff --git a/src/i18n/message/popup/index.ts b/src/i18n/message/popup/index.ts index fd983194e..9a404180d 100644 --- a/src/i18n/message/popup/index.ts +++ b/src/i18n/message/popup/index.ts @@ -7,6 +7,7 @@ import menuMessages, { MenuMessage } from "../app/menu" import baseMessages, { BaseMessage } from "../common/base" +import calendarMessages, { CalendarMessage } from "../common/calendar" import itemMessages, { ItemMessage } from "../common/item" import metaMessages, { MetaMessage } from "../common/meta" import { merge, MessageRoot } from "../merge" @@ -20,6 +21,7 @@ export type PopupMessage = { base: BaseMessage footer: FooterMessage menu: MenuMessage + calendar: CalendarMessage } const MESSAGE_ROOT: MessageRoot = { @@ -29,6 +31,7 @@ const MESSAGE_ROOT: MessageRoot = { base: baseMessages, footer: footerMessages, menu: menuMessages, + calendar: calendarMessages, } const _default = merge(MESSAGE_ROOT) diff --git a/src/popup/common.ts b/src/popup/common.ts index 38339876e..2cb70b28c 100644 --- a/src/popup/common.ts +++ b/src/popup/common.ts @@ -5,10 +5,24 @@ const KEYS: { [duration in timer.option.PopupDuration]: () => string } = { today: () => t(calendarMessages, { key: msg => msg.range.today }), thisWeek: () => t(calendarMessages, { key: msg => msg.range.thisWeek }), thisMonth: () => t(calendarMessages, { key: msg => msg.range.thisMonth }), - last30Days: () => t(calendarMessages, { key: msg => msg.range.lastDays, param: { n: 30 } }) + last30Days: () => t(calendarMessages, { key: msg => msg.range.lastDays, param: { n: 30 } }), + allTime: () => t(calendarMessages, { key: msg => msg.range.allTime }) } export const durationLabelOf = (duration: timer.option.PopupDuration): string => { const key = KEYS[duration] return t(calendarMessages, { key }) -} \ No newline at end of file +} + +export type PopupRow = timer.stat.Row & { isOther?: boolean } + +export type PopupQueryResult = { + type: timer.stat.Dimension + mergeHost: boolean + data: PopupRow[] + dataDate: [string, string] + // Filter items + chartTitle: string + date: Date | [Date, Date?] + dateLength: number +} diff --git a/src/popup/components/chart/click-handler.ts b/src/popup/components/chart/click-handler.ts index 3dd0fb094..59a755ab9 100644 --- a/src/popup/components/chart/click-handler.ts +++ b/src/popup/components/chart/click-handler.ts @@ -1,15 +1,16 @@ /** * Copyright (c) 2021 Hengyang Zhang - * + * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { PopupQueryResult, PopupRow } from "@popup/common" import type { CallbackDataParams } from "echarts/types/dist/shared" +import { createTab } from "@api/chrome/tab" import { REPORT_ROUTE } from "@app/router/constants" import { getAppPageUrl } from "@util/constant/url" -import { createTab } from "@api/chrome/tab" function generateUrl(data: PopupRow, queryResult: PopupQueryResult): string { const { host, isOther } = data diff --git a/src/popup/components/chart/index.ts b/src/popup/components/chart/index.ts index 6c7194b12..2bd359b69 100644 --- a/src/popup/components/chart/index.ts +++ b/src/popup/components/chart/index.ts @@ -8,19 +8,19 @@ import type { ECharts } from "echarts/core" import type { CallbackDataParams } from "echarts/types/dist/shared" -import { init, use } from "echarts/core" +import OptionDatabase from "@db/option-database" +import { PopupQueryResult } from "@popup/common" +import { defaultStatistics } from "@util/constant/option" import { PieChart } from "echarts/charts" -import { TitleComponent, ToolboxComponent, TooltipComponent, LegendComponent } from "echarts/components" +import { LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent } from "echarts/components" +import { init, use } from "echarts/core" import { SVGRenderer } from "echarts/renderers" +import handleClick from "./click-handler" +import { pieOptions } from "./option" // Register echarts use([TitleComponent, ToolboxComponent, TooltipComponent, LegendComponent, SVGRenderer, PieChart]) -import { defaultStatistics } from "@util/constant/option" -import OptionDatabase from "@db/option-database" -import handleClick from "./click-handler" -import { pieOptions } from "./option" - const optionDatabase = new OptionDatabase(chrome.storage.local) const chartContainer = document.getElementById('chart-container') as HTMLDivElement diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index 6a3ed48f4..e25412025 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import type { PopupQueryResult, PopupRow } from "@popup/common" import type { PieSeriesOption } from "echarts/charts" import type { LegendComponentOption, @@ -21,7 +22,7 @@ import { IS_SAFARI } from "@util/constant/environment" import { getAppPageUrl } from "@util/constant/url" import { generateSiteLabel } from "@util/site" import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" -import { formatPeriodCommon, formatTime } from "@util/time" +import { formatPeriodCommon, formatTime, parseTime } from "@util/time" import { optionIcon } from "./toolbox-icon" type EcOption = ComposeOption< @@ -145,30 +146,59 @@ function calcPositionOfTooltip(container: HTMLDivElement, point: (number | strin return [...point] } -const Y_M_D = "{y}/{m}/{d}" -function calculateSubTitleText(date: Date | [Date, Date?]) { - if (date instanceof Array) { - const [start, _] = date - const startStr = formatTime(start, Y_M_D) - let endStr = formatTime(new Date(), Y_M_D) - if (startStr === endStr) { - return startStr - } else { - if (startStr.substring(0, 4) === endStr.substring(0, 4)) { - // the same year - endStr = endStr.substring(5) - } - return `${startStr}-${endStr}` - } - } else { - return formatTime(date, Y_M_D) +function calculateSubTitleText(date: Date | [Date, Date?], dataDate: [string, string]): string { + const format = t(msg => msg.calendar.dateFormat) + + if (!date) { + date = dataDate?.map(parseTime) as [Date, Date] + } else if (!(date instanceof Array)) { + // Single day + return formatTime(date, format) } + + const [start, end] = date + if (!start && !end) return '' + if (!start) return formatTime(end, format) + if (!end) return formatTime(start, format) + + return combineDate(start, end, format) +} + +function combineDate(start: Date, end: Date, format: string): string { + const startStr = formatTime(start, format) + const endStr = formatTime(end, format) + if (startStr === endStr) { + return startStr + } + const normalStr = `${startStr}-${endStr}` + + const sy = start.getFullYear() + const ey = end.getFullYear() + if (sy !== ey) { + // Different years + return normalStr + } + + // The same years + const execRes = /({d}|{m})[^{}]*({d}|{m})/.exec(format) + let monthDatePart = execRes?.[0] + + if (!monthDatePart) return normalStr + + const newPart = `${monthDatePart}-${monthDatePart.replace('{m}', '{em}').replace('{d}', '{ed}')}` + const newFormat = format.replace(monthDatePart, newPart) + const em = end.getMonth() + 1 + const ed = end.getDate() + return formatTime(start, newFormat) + .replace('{em}', em.toString().padStart(2, '0')) + .replace('{ed}', ed.toString().padStart(2, '0')) } export function pieOptions(props: ChartProps, container: HTMLDivElement): EcOption { const { type, data, displaySiteName, chartTitle, date } = props const titleText = chartTitle - const subTitleText = `${calculateSubTitleText(date)} @ ${t(msg => msg.meta.name)}` + const dateText = calculateSubTitleText(date, props.dataDate) + const subTitleText = `${dateText} @ ${t(msg => msg.meta.name)}` const textColor = getPrimaryTextColor() const secondaryColor = getSecondaryTextColor() const options: EcOption = { diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index f957a9527..c43b9b80a 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -5,19 +5,20 @@ * https://opensource.org/licenses/MIT */ +import type { PopupQueryResult, PopupRow } from "@popup/common" import type { StatQueryParam } from "@service/stat-service" +import { locale } from "@i18n" +import { t } from "@popup/locale" +import optionService from "@service/option-service" +import statService from "@service/stat-service" +import { getDayLength, getMonthTime, getWeekTime, MILL_PER_DAY } from "@util/time" import initAllFunction from './all-function' -import initUpgrade from './upgrade' -import TotalInfoWrapper from "./total-info" import MergeHostWrapper from "./merge-host" import TimeSelectWrapper from "./select/time-select" import TypeSelectWrapper from "./select/type-select" -import statService from "@service/stat-service" -import { t } from "@popup/locale" -import { locale } from "@i18n" -import { getDayLength, getMonthTime, getWeekTime, MILL_PER_DAY } from "@util/time" -import optionService from "@service/option-service" +import TotalInfoWrapper from "./total-info" +import initUpgrade from './upgrade' type FooterParam = StatQueryParam & { chartTitle: string @@ -27,13 +28,24 @@ type QueryResultHandler = (result: PopupQueryResult) => void type DateRangeCalculator = (now: Date, weekStart: timer.option.WeekStartOption) => Date | [Date, Date] -const dateRangeCalculators: { [duration in PopupDuration]: DateRangeCalculator } = { +const dateRangeCalculators: { [duration in timer.option.PopupDuration]: DateRangeCalculator } = { today: now => 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], + allTime: () => null, } +const otherPopupRow = (): PopupRow => ({ + host: t(msg => msg.chart.otherLabel, { count: 0 }), + focus: 0, + date: '0000-00-00', + time: 0, + mergedHosts: [], + isOther: true, + virtual: false +}) + class FooterWrapper { private afterQuery: QueryResultHandler private timeSelectWrapper: TimeSelectWrapper @@ -70,15 +82,7 @@ class FooterWrapper { const queryParam = this.getQueryParam(option.weekStart) const rows = await statService.select(queryParam, true) const popupRows: PopupRow[] = [] - const other: PopupRow = { - host: t(msg => msg.chart.otherLabel, { count: 0 }), - focus: 0, - date: '0000-00-00', - time: 0, - mergedHosts: [], - isOther: true, - virtual: false - } + const other = otherPopupRow() let otherCount = 0 for (let i = 0; i < rows.length; i++) { const row = rows[i] @@ -94,9 +98,15 @@ class FooterWrapper { const type = queryParam.sort as timer.stat.Dimension const data = popupRows.filter(item => item[type]) const date = queryParam.date + let mixDate: string, maxDate: string + rows.flatMap(r => r.mergedDates || []).map(d => { + if (!mixDate || d < mixDate) mixDate = d + if (!maxDate || d > maxDate) maxDate = d + }) const queryResult: PopupQueryResult = { data, + dataDate: [mixDate, maxDate], mergeHost: queryParam.mergeHost, type, date, @@ -108,7 +118,7 @@ class FooterWrapper { } getQueryParam(weekStart: timer.option.WeekStartOption): FooterParam { - const duration: PopupDuration = this.timeSelectWrapper.getSelectedTime() + const duration = this.timeSelectWrapper.getSelectedTime() const param: FooterParam = { date: dateRangeCalculators[duration]?.(new Date(), weekStart), mergeHost: this.mergeHostWrapper.mergedHost(), diff --git a/src/popup/components/footer/select/time-select.ts b/src/popup/components/footer/select/time-select.ts index 75651b97e..a2e3ee93c 100644 --- a/src/popup/components/footer/select/time-select.ts +++ b/src/popup/components/footer/select/time-select.ts @@ -5,7 +5,6 @@ * https://opensource.org/licenses/MIT */ -// Time select import { durationLabelOf } from "@popup/common" import { ALL_POPUP_DURATION } from "@util/constant/popup" @@ -15,17 +14,17 @@ class TimeSelectWrapper { private timeSelectPopup: HTMLElement private timeSelectInput: HTMLInputElement private isOpen: boolean = false - private currentSelected: PopupDuration = undefined + private currentSelected: timer.option.PopupDuration = undefined private handleSelected: Function private optionList: HTMLElement - private optionItems: Map = new Map() + private optionItems: Map = new Map() constructor(handleSelected: Function) { this.handleSelected = handleSelected } - async init(initialVal: PopupDuration): Promise { + async init(initialVal: timer.option.PopupDuration): Promise { this.timeSelect = document.getElementById('time-select-container') this.timeSelectPopup = document.getElementById('time-select-popup') this.timeSelectInput = document.getElementById('time-select-input') as HTMLInputElement @@ -38,7 +37,7 @@ class TimeSelectWrapper { this.selected(initialVal) } - private initOption(item: PopupDuration) { + private initOption(item: timer.option.PopupDuration) { const li = document.createElement('li') li.classList.add('el-select-dropdown__item') li.innerText = durationLabelOf(item) @@ -51,7 +50,7 @@ class TimeSelectWrapper { this.optionItems.set(item, li) } - private selected(item: PopupDuration) { + private selected(item: timer.option.PopupDuration) { this.currentSelected = item Array.from(this.optionItems.values()).forEach(item => item.classList.remove(SELECTED_CLASS)) this.optionItems.get(item).classList.add(SELECTED_CLASS) @@ -70,7 +69,7 @@ class TimeSelectWrapper { this.isOpen = false } - getSelectedTime(): PopupDuration { + getSelectedTime(): timer.option.PopupDuration { return this.currentSelected } } diff --git a/src/popup/popup.d.ts b/src/popup/popup.d.ts deleted file mode 100644 index d4ac27494..000000000 --- a/src/popup/popup.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -type PopupRow = timer.stat.Row & { isOther?: boolean } - -type PopupQueryResult = { - type: timer.stat.Dimension - mergeHost: boolean - data: PopupRow[] - // Filter items - chartTitle: string - date: Date | [Date, Date?] - dateLength: number -} - -type PopupDuration = timer.option.PopupDuration \ No newline at end of file diff --git a/src/popup/style/index.sass b/src/popup/style/index.sass index 964b3d118..61bd0c98d 100644 --- a/src/popup/style/index.sass +++ b/src/popup/style/index.sass @@ -69,7 +69,7 @@ body #time-select-popup z-index: 2004 left: 489px - top: 342px + top: 308px margin: 0px .el-svg-icon diff --git a/src/service/stat-service/merge.ts b/src/service/stat-service/merge.ts index 99d52f268..4c19d7026 100644 --- a/src/service/stat-service/merge.ts +++ b/src/service/stat-service/merge.ts @@ -70,14 +70,20 @@ function accCompositionValue(map: _RemoteCompositionMap, value: timer.stat.Remot } } -export function mergeDate(origin: T[]): T[] { - const map: Record = {} - origin.forEach(o => merge(map, o, o.host).date = '') +export function mergeDate(origin: timer.stat.Row[]): timer.stat.Row[] { + const map: Record = {} + origin.forEach(o => { + let merged = merge(map, o, o.host) + merged.date = null + let mergedDates = merged.mergedDates || [] + mergedDates.push(o.date) + merged.mergedDates = mergedDates + }) const newRows = Object.values(map) return newRows } -export async function mergeHost(origin: T[]): Promise { +export async function mergeHost(origin: timer.stat.Row[]): Promise { const newRows = [] const map = {} diff --git a/src/util/constant/popup.ts b/src/util/constant/popup.ts index 18b4b9581..1346dcc8f 100644 --- a/src/util/constant/popup.ts +++ b/src/util/constant/popup.ts @@ -5,4 +5,6 @@ * https://opensource.org/licenses/MIT */ -export const ALL_POPUP_DURATION: PopupDuration[] = ["today", "thisWeek", "thisMonth", "last30Days"] \ No newline at end of file +export const ALL_POPUP_DURATION: timer.option.PopupDuration[] = [ + "today", "thisWeek", "thisMonth", "last30Days", "allTime", +] \ No newline at end of file diff --git a/types/timer/option.d.ts b/types/timer/option.d.ts index 30856f1b8..afc76ce47 100644 --- a/types/timer/option.d.ts +++ b/types/timer/option.d.ts @@ -7,6 +7,7 @@ declare namespace timer.option { type PopupDuration = | "today" | "thisWeek" | "thisMonth" | "last30Days" + | "allTime" /** * Options used for the popup page */ diff --git a/types/timer/stat.d.ts b/types/timer/stat.d.ts index 919db0e2e..12a47747a 100644 --- a/types/timer/stat.d.ts +++ b/types/timer/stat.d.ts @@ -53,6 +53,12 @@ declare namespace timer.stat { * @since 0.1.5 */ mergedHosts: Row[] + /** + * The merged dates + * + * @since 2.4.7 + */ + mergedDates?: string[] /** * Whether virtual host * From c5c64dac432f8b462c7d5790c3645fff69ff4c33 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 28 Sep 2024 17:01:30 +0800 Subject: [PATCH 6/9] Fix style of popup page (#318) --- src/i18n/message/common/base-resource.json | 2 +- src/i18n/message/common/item-resource.json | 2 +- src/i18n/message/popup/footer-resource.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/message/common/base-resource.json b/src/i18n/message/common/base-resource.json index c136223df..02d0a1b7c 100644 --- a/src/i18n/message/common/base-resource.json +++ b/src/i18n/message/common/base-resource.json @@ -56,7 +56,7 @@ "changeLog": "Änderungsprotokoll" }, "fr": { - "allFunction": "Toutes les fonctions", + "allFunction": "Page back-end", "guidePage": "Manuel d'utilisateur", "sourceCode": "Code Source", "changeLog": "Historique des changements" diff --git a/src/i18n/message/common/item-resource.json b/src/i18n/message/common/item-resource.json index 5e6d7c81e..fd37adf6f 100644 --- a/src/i18n/message/common/item-resource.json +++ b/src/i18n/message/common/item-resource.json @@ -156,7 +156,7 @@ "date": "Дата", "host": "URL-адрес сайта", "focus": "Время просмотра", - "time": "Количество посещений", + "time": "Посещения", "operation": { "add2Whitelist": "Белый список", "removeFromWhitelist": "Включить", diff --git a/src/i18n/message/popup/footer-resource.json b/src/i18n/message/popup/footer-resource.json index aa2ec3a3e..a025cdab7 100644 --- a/src/i18n/message/popup/footer-resource.json +++ b/src/i18n/message/popup/footer-resource.json @@ -51,7 +51,7 @@ "updateVersion": "Actualisable", "updateVersionInfo": "Dernière version : {version}", "updateVersionInfo4Firefox": "Mettez à niveau vers là {version} dans la page de gestion, about:addons, s'il vous plaît", - "rate": "Donnez votre avis" + "rate": "Avis" }, "ru": { "updateVersion": "Обновляемый", From 232ea2fd9b5d33a59318f55b945744aa0c696fa4 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 28 Sep 2024 17:03:47 +0800 Subject: [PATCH 7/9] Upgrade deps --- package.json | 16 ++++++++-------- src/database/common/storage-promise.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8401724ca..1adef1b31 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,14 @@ "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/preset-env": "^7.25.4", - "@crowdin/crowdin-api-client": "^1.35.0", - "@types/chrome": "0.0.271", + "@crowdin/crowdin-api-client": "^1.36.0", + "@types/chrome": "0.0.272", "@types/copy-webpack-plugin": "^10.1.0", "@types/decompress": "^4.2.7", "@types/echarts": "^4.9.22", "@types/generate-json-webpack-plugin": "^0.3.7", "@types/jest": "^29.5.13", - "@types/node": "^22.5.5", + "@types/node": "^22.7.4", "@types/psl": "^1.1.3", "@types/punycode": "^2.1.4", "@types/webpack": "^5.28.5", @@ -43,14 +43,14 @@ "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "decompress": "^4.2.1", - "eslint": "^9.11.0", + "eslint": "^9.11.1", "filemanager-webpack-plugin": "^8.0.0", "generate-json-webpack-plugin": "^2.0.0", "html-webpack-plugin": "^5.6.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.1", - "sass": "^1.79.3", + "sass": "^1.79.4", "sass-loader": "^16.0.2", "style-loader": "^4.0.0", "ts-jest": "^29.2.5", @@ -60,7 +60,7 @@ "tslib": "^2.7.0", "typescript": "5.6.2", "url-loader": "^4.1.1", - "webpack": "^5.94.0", + "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4" }, @@ -69,11 +69,11 @@ "@vueuse/core": "^11.1.0", "countup.js": "^2.8.0", "echarts": "^5.5.1", - "element-plus": "2.8.3", + "element-plus": "2.8.4", "js-base64": "^3.7.7", "punycode": "^2.3.1", "stream-browserify": "^3.0.0", - "vue": "^3.5.7", + "vue": "^3.5.10", "vue-router": "^4.4.5" }, "engines": { diff --git a/src/database/common/storage-promise.ts b/src/database/common/storage-promise.ts index 43c22a8c1..e9f1aad91 100644 --- a/src/database/common/storage-promise.ts +++ b/src/database/common/storage-promise.ts @@ -5,6 +5,11 @@ * https://opensource.org/licenses/MIT */ +/** + * Copy from chrome.storage + */ +type NoInferX = T[][T extends any ? 0 : never] + /** * Wrap the storage with promise */ @@ -15,7 +20,9 @@ export default class StoragePromise { this.storage = storage } - get(keys?: string | string[] | Object | null): Promise<{ [key: string]: any }> { + get( + keys?: NoInferX | Array> | Partial> | null, + ): Promise<{ [key: string]: any }> { return new Promise(resolve => this.storage.get(keys, resolve)) } From 111b4945d7de78790d0dd5e59c6410b2cce90d25 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sat, 28 Sep 2024 17:15:13 +0800 Subject: [PATCH 8/9] v2.4.7 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d86f1b29..da2b2cfef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Time Tracker will be documented in this file. It is worth mentioning that the release time of each change refers to the time when the installation package is submitted to the webstore. It is about one week for Edge to moderate packages, while only 1-2 days for Chrome and Firefox. +## [2.4.7] - 2024-09-28 + +- Fixed some style bugs +- Supported all time's data on popup page + ## [2.4.6] - 2024-09-22 - Fixed some bugs diff --git a/package.json b/package.json index 1adef1b31..718670f93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timer", - "version": "2.4.6", + "version": "2.4.7", "description": "Time tracker", "homepage": "https://www.wfhg.cc", "scripts": { From d1860f805f27632711ff416e08c817e27b2d4880 Mon Sep 17 00:00:00 2001 From: sheepzh Date: Sun, 8 Dec 2024 17:19:09 +0800 Subject: [PATCH 9/9] Semi --- src/api/quantified-resume.ts | 1 + src/common/backup/quantified-resume/coordinator.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/quantified-resume.ts b/src/api/quantified-resume.ts index 6d7b1ba1b..826852863 100644 --- a/src/api/quantified-resume.ts +++ b/src/api/quantified-resume.ts @@ -26,6 +26,7 @@ export type Item = { metrics: { visit: number focus: number + host: string } payload?: Record } diff --git a/src/common/backup/quantified-resume/coordinator.ts b/src/common/backup/quantified-resume/coordinator.ts index 23f48d02e..d58a4b39d 100644 --- a/src/common/backup/quantified-resume/coordinator.ts +++ b/src/common/backup/quantified-resume/coordinator.ts @@ -109,7 +109,7 @@ export default class QuantifiedResumeCoordinator implements timer.backup.Coordin const item: Item = { refId: `${date}${host}`, timestamp: time.getTime(), - metrics: { visit, focus }, + metrics: { visit, focus, host }, action: "web_time", name: host, payload: { date, host, cid: context.cid },