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
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Time tracker for browser",
"homepage": "https://www.wfhg.cc",
"scripts": {
"pure-install": "npm install --include=optional --ignore-scripts",
"dev": "rspack --config=rspack/rspack.dev.ts --watch",
"dev:firefox": "rspack --config=rspack/rspack.dev.firefox.ts --watch",
"dev:safari": "rspack --config=rspack/rspack.dev.safari.ts --watch",
Expand Down Expand Up @@ -34,7 +35,7 @@
"@rsdoctor/rspack-plugin": "^1.2.3",
"@rspack/cli": "^1.4.11",
"@rspack/core": "^1.4.11",
"@swc/core": "^1.13.3",
"@swc/core": "^1.13.5",
"@swc/jest": "^0.2.39",
"@types/chrome": "0.1.4",
"@types/decompress": "^4.2.7",
Expand All @@ -57,7 +58,7 @@
"sass": "^1.90.0",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.2",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "5.9.2",
Expand All @@ -71,7 +72,7 @@
"@vueuse/core": "^13.7.0",
"countup.js": "^2.9.0",
"echarts": "^6.0.0",
"element-plus": "2.10.7",
"element-plus": "2.11.1",
"js-base64": "^3.7.8",
"punycode": "^2.3.1",
"stream-browserify": "^3.0.0",
Expand All @@ -81,4 +82,4 @@
"engines": {
"node": ">=20"
}
}
}
6 changes: 3 additions & 3 deletions script/crowdin/sync-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type SourceFilesModel, type SourceStringsModel } from "@crowdin/crowdin-api-client"
import { groupBy } from "@util/array"
import { toMap } from "@util/array"
import { type CrowdinClient, getClientFromEnv, type NameKey } from "./client"
import {
ALL_DIRS,
Expand Down Expand Up @@ -33,7 +33,7 @@ async function processStrings(
fileContent: ItemSet,
) {
const existStrings = await client.listStringsByFile(existFile.id)
const existStringsKeyMap = groupBy(existStrings, s => s.identifier, l => l[0])
const existStringsKeyMap = toMap(existStrings, s => s.identifier)
const strings2Delete: SourceStringsModel.String[] = []
const strings2Create: ItemSet = {}
const strings2Update: ItemSet = {}
Expand Down Expand Up @@ -70,7 +70,7 @@ async function processByDir(client: CrowdinClient, dir: Dir, branch: SourceFiles
// 3. list all files in directory
const existFiles = await client.listFilesByDirectory(directory.id)
console.log("Exists file count: " + existFiles.length)
const existFileNameMap = groupBy(existFiles, f => f.name, l => l[0])
const existFileNameMap = toMap(existFiles, f => f.name)
// 4. create new files
for (const [fileName, msg] of Object.entries(messages)) {
if (isIgnored(dir, fileName)) {
Expand Down
6 changes: 3 additions & 3 deletions script/crowdin/sync-translation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { type SourceFilesModel } from "@crowdin/crowdin-api-client"
import { groupBy } from "@util/array"
import { toMap } from "@util/array"
import { exitWith } from "../util/process"
import { type CrowdinClient, getClientFromEnv } from "./client"
import {
Expand All @@ -14,7 +14,7 @@ const CROWDIN_USER_ID_OF_OWNER = 15266594
async function processDirMessage(client: CrowdinClient, file: SourceFilesModel.File, message: ItemSet, lang: CrowdinLanguage): Promise<void> {
console.log(`Start to process dir message: fileName=${file.name}, lang=${lang}`)
const strings = await client.listStringsByFile(file.id)
const stringMap = groupBy(strings, s => s.identifier, l => l[0])
const stringMap = toMap(strings, s => s.identifier)
for (const [identifier, text] of Object.entries(message)) {
const string = stringMap[identifier]
if (!string) {
Expand Down Expand Up @@ -52,7 +52,7 @@ async function processDir(client: CrowdinClient, dir: Dir, branch: SourceFilesMo
}
const files = await client.listFilesByDirectory(directory!.id)
console.log(`find ${files.length} files of ${dir}`)
const fileMap = groupBy(files, f => f.name, l => l[0])
const fileMap = toMap(files, f => f.name)
for (const [fileName, message] of Object.entries(messages)) {
console.log(`Start to sync translations of ${dir}/${fileName}`)
if (isIgnored(dir, fileName)) {
Expand Down
2 changes: 2 additions & 0 deletions src/background/content-script-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import optionHolder from "@service/components/option-holder"
import whitelistHolder from "@service/components/whitelist-holder"
import limitService from "@service/limit-service"
import siteService from "@service/site-service"
import { saveTimelineEvent } from '@service/timeline-service'
import { getAppPageUrl } from "@util/constant/url"
import { extractFileHost, extractHostname } from "@util/pattern"
import badgeManager from "./badge-manager"
Expand Down Expand Up @@ -73,4 +74,5 @@ export default function init(dispatcher: MessageDispatcher) {
const exist = await siteService.get(site)
return exist?.run ? site : null
})
.register<timer.timeline.Event, void>('cs.timelineEv', ev => saveTimelineEvent(ev))
}
2 changes: 1 addition & 1 deletion src/background/track-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async function handleIncVisitEvent(param: { host: string, url: string }, sender:
function splitRunTime(start: number, end: number): Record<string, number> {
const res: Record<string, number> = {}
while (start < end) {
const startOfNextDay = getStartOfDay(new Date(start)).getTime() + MILL_PER_DAY
const startOfNextDay = getStartOfDay(start).getTime() + MILL_PER_DAY
const newStart = Math.min(end, startOfNextDay)
const runTime = newStart - start
runTime && (res[formatTimeYMD(start)] = runTime)
Expand Down
3 changes: 3 additions & 0 deletions src/content-script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { sendMsg2Runtime } from "@api/chrome/runtime"
import { initLocale } from "@i18n"
import processLimit from "./limit"
import printInfo from "./printer"
import processTimeline from './timeline'
import NormalTracker from "./tracker/normal"
import RunTimeTracker from "./tracker/run-time"

Expand Down Expand Up @@ -73,6 +74,8 @@ async function main() {
!!needPrintInfo && printInfo(host)
await processLimit(url)

processTimeline()

// Increase visit count at the end
await sendMsg2Runtime('cs.incVisitCount', { host, url })
}
Expand Down
57 changes: 57 additions & 0 deletions src/content-script/timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { sendMsg2Runtime } from '@api/chrome/runtime'

class TimelineCollector {
private startTime: number | null = null

/**
* Bind page visibility and focus events
*/
init(): void {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.collect()
} else {
this.startTracking()
}
})

window.addEventListener('focus', () => this.startTracking())
window.addEventListener('blur', () => this.collect())
window.addEventListener('beforeunload', () => this.collect())

if (document.readyState === 'complete') {
this.startTracking()
} else {
window.addEventListener('load', () => this.startTracking())
}
}

/**
* Start tracking current page
*/
public startTracking(): void {
this.startTime = Date.now()
}

/**
* End current session and generate event
*/
private collect(): void {
if (!this.startTime) return
const url = document?.location?.href

url && sendMsg2Runtime('cs.timelineEv', {
start: this.startTime,
end: Date.now(),
url,
} satisfies timer.timeline.Event)

this.startTime = null
}
}


export default function processTimeline() {
const collector = new TimelineCollector()
collector.init()
}
2 changes: 1 addition & 1 deletion src/database/site-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function cvt2SiteInfo(key: timer.site.SiteKey, entry: _Entry | undefined): timer
const { a, i, c, r } = entry || {}
const siteInfo: timer.site.SiteInfo = { ...key }
siteInfo.alias = a
siteInfo.cate = c
siteInfo.cate = c ?? CATE_NOT_SET_ID
siteInfo.iconUrl = i
siteInfo.run = !!r
return siteInfo
Expand Down
101 changes: 101 additions & 0 deletions src/database/timeline-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { formatTimeYMD, MILL_PER_DAY } from '@util/time'
import BaseDatabase from './common/base-database'
import { REMAIN_WORD_PREFIX } from './common/constant'

const DB_KEY = REMAIN_WORD_PREFIX + 'TL'

type Item = {
// start
s: number
// duration
d: number
}

type TimelineData = {
[date: string]: {
[host: string]: Item[]
}
}

// If two tick with the same host is near 1 sec, then merge them to one
const MERGE_THRESHOLD = 1000

const canMerge = (item: Item, tick: timer.timeline.Tick) => {
const { s: is, d: id } = item
const { start } = tick
return start >= is + id
&& start <= id + MERGE_THRESHOLD
}

const isConflict = (item: Item, tick: timer.timeline.Tick) => {
const { s: is, d: id } = item
const { start } = tick
return is <= start && start < is + id
}

const merge = (data: TimelineData, tick: timer.timeline.Tick) => {
const { start, duration, host } = tick
const date = formatTimeYMD(start)
const hostData = data[date] ?? {}
const items = hostData[host] ?? []
items.sort((a, b) => (a?.s ?? 0) - (b?.s ?? 0))
for (const item of items) {
if (isConflict(item, tick)) {
return
}
if (canMerge(item, tick)) {
item.d = start + duration - item.s
return
}
}
// normal tick
items.push({ s: start, d: duration })
hostData[host] = items
data[date] = hostData
}

export const TIMELINE_LIFE_CYCLE = 3

const removeOutdated = (data: TimelineData, currTime: number) => {
const minDate = formatTimeYMD(currTime - MILL_PER_DAY * (TIMELINE_LIFE_CYCLE - 1))
const keys = Object.keys(data).filter(k => k < minDate)
keys.forEach(key => delete data[key])
}

class TimelineDatabase extends BaseDatabase {
private async getData(): Promise<TimelineData> {
const data = await this.storage.getOne<TimelineData>(DB_KEY)
return data ?? {}
}

private setData(data: TimelineData): Promise<void> {
return this.setByKey(DB_KEY, data)
}

async batchSave(ticks: timer.timeline.Tick[]) {
const data = await this.getData()
ticks.forEach(tick => {
merge(data, tick)
removeOutdated(data, tick.start)
})
await this.setData(data)
}

async getAll(): Promise<timer.timeline.Tick[]> {
const data = await this.getData()
const result: timer.timeline.Tick[] = []
Object.values(data).forEach(hostData => {
Object.entries(hostData).forEach(([host, items]) => {
items.forEach(({ s: start, d: duration }) => result.push({ host, start, duration }))
})
})
return result
}

async importData(_: any): Promise<void> {
// do nothing
}
}
const timelineDatabase = new TimelineDatabase()

export default timelineDatabase
14 changes: 14 additions & 0 deletions src/i18n/message/app/dashboard-resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
},
"monthOnMonth": {
"title": "浏览时间环比趋势"
},
"timeline": {
"title": "近 {n} 天时间线",
"busyScore": "忙碌指数",
"busyScoreDesc": "与每小时浏览的总时长和网站数量有关,详细计算公式请查看源代码",
"focusScore": "专注指数",
"focusScoreDesc": "与同一网站连续浏览总时长有关,详细计算公式请查看源代码"
}
},
"zh_TW": {
Expand Down Expand Up @@ -51,6 +58,13 @@
},
"monthOnMonth": {
"title": "Browsing time month-on-month trend"
},
"timeline": {
"title": "Timeline of the recent {n} days",
"busyScore": "Busy Score",
"busyScoreDesc": "Related to the total browsing time and the number of websites per hour. See the source code for calculation formula",
"focusScore": "Focus Score",
"focusScoreDesc": "Related to the total time of continuous browsing of the same website. See the source code for calculation formula"
}
},
"ja": {
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/message/app/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ export type DashboardMessage = {
monthOnMonth: {
title: string
}
timeline: {
title: string
busyScore: string
busyScoreDesc: string
focusScore: string
focusScoreDesc: string
}
}

const _default: Messages<DashboardMessage> = resource
Expand Down
Loading