Skip to content

Commit 4c0d96a

Browse files
authored
feat: support track the time of tab groups (sheepzh#481) (sheepzh#482)
1 parent a833044 commit 4c0d96a

File tree

140 files changed

+2007
-1464
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+2007
-1464
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@
6161
"ts-node": "^10.9.2",
6262
"tsconfig-paths": "^4.2.0",
6363
"typescript": "5.8.3",
64-
"url-loader": "^4.1.1",
64+
"url-loader": "^4.1.1"
65+
},
66+
"optionalDependencies": {
6567
"web-ext": "^8.7.1"
6668
},
6769
"dependencies": {

src/api/chrome/permission.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { IS_MV3 } from "@util/constant/environment"
2+
import { handleError } from "./common"
3+
4+
export async function hasPerm(perm: chrome.runtime.ManifestPermissions): Promise<boolean> {
5+
if (IS_MV3) {
6+
try {
7+
return await chrome.permissions.contains({ permissions: [perm] })
8+
} catch {
9+
return false
10+
}
11+
} else {
12+
return new Promise(resolve => {
13+
chrome.permissions.contains({ permissions: [perm] }, res => {
14+
handleError('hasPerm')
15+
resolve(!!res)
16+
})
17+
})
18+
}
19+
}
20+
21+
export async function requestPerm(perm: chrome.runtime.ManifestPermissions): Promise<boolean> {
22+
if (IS_MV3) {
23+
try {
24+
return await chrome.permissions.request({ permissions: [perm] })
25+
} catch {
26+
return false
27+
}
28+
} else {
29+
return new Promise(resolve => {
30+
chrome.permissions.request({ permissions: [perm] }, granted => {
31+
handleError("requestPerm")
32+
resolve(!!granted)
33+
})
34+
})
35+
}
36+
}
37+
38+
export async function onPermRemoved(callback: ArgCallback<chrome.permissions.Permissions>) {
39+
chrome.permissions.onRemoved.addListener(callback)
40+
}

src/api/chrome/tabGroups.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { IS_MV3 } from "@util/constant/environment"
2+
import { handleError } from "./common"
3+
4+
export async function listAllGroups(): Promise<chrome.tabGroups.TabGroup[]> {
5+
if (IS_MV3) {
6+
try {
7+
return chrome.tabGroups.query({})
8+
} catch (e) {
9+
return []
10+
}
11+
} else {
12+
return new Promise(resolve => {
13+
chrome.tabGroups.query({}, arr => {
14+
handleError('listAllGroups')
15+
resolve(arr ?? [])
16+
})
17+
})
18+
}
19+
}
20+
21+
export async function getGroup(id: number | undefined): Promise<chrome.tabGroups.TabGroup | undefined> {
22+
if (!id) return undefined
23+
try {
24+
if (IS_MV3) {
25+
const group = await chrome.tabGroups.get(id)
26+
return group
27+
} else {
28+
return new Promise(resolve => chrome.tabGroups.get(id, g => {
29+
handleError('getGroup')
30+
resolve(g)
31+
}))
32+
}
33+
} catch (e) {
34+
return undefined
35+
}
36+
}
37+
38+
export function onChanged(handler: ArgCallback<chrome.tabGroups.TabGroup>): void {
39+
try {
40+
chrome.tabGroups.onCreated.addListener(handler)
41+
chrome.tabGroups.onRemoved.addListener(handler)
42+
chrome.tabGroups.onUpdated.addListener(handler)
43+
} catch (e) {
44+
// ignored
45+
}
46+
}
47+
48+
export function removeChangedHandler(handler: ArgCallback<chrome.tabGroups.TabGroup>): void {
49+
try {
50+
chrome.tabGroups.onCreated.removeListener(handler)
51+
chrome.tabGroups.onRemoved.removeListener(handler)
52+
chrome.tabGroups.onUpdated.removeListener(handler)
53+
} catch (e) {
54+
// ignored
55+
}
56+
}
57+
58+
export function isValidGroup(groupId?: number): groupId is number {
59+
if (!groupId) return false
60+
try {
61+
return !!groupId && groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE
62+
} catch {
63+
return false
64+
}
65+
}
66+
67+
export const cvtGroupColor = (color?: `${chrome.tabGroups.Color}`): string => {
68+
switch (color) {
69+
case 'grey': return '#5F6369'
70+
case 'blue': return '#1974E8'
71+
case 'yellow': return '#F9AB03'
72+
case 'red': return '#DA3025'
73+
case 'green': return '#198139'
74+
case 'pink': return '#D01984'
75+
case 'purple': return '#A143F5'
76+
case 'cyan': return '#027B84'
77+
case 'orange': return '#FA913E'
78+
default: return '#000'
79+
}
80+
}

src/background/badge-manager.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@
88
import { setBadgeBgColor, setBadgeText } from "@api/chrome/action"
99
import { listTabs } from "@api/chrome/tab"
1010
import { getFocusedNormalWindow } from "@api/chrome/window"
11-
import StatDatabase from "@db/stat-database"
11+
import statDatabase from "@db/stat-database"
1212
import optionHolder from "@service/components/option-holder"
1313
import whitelistHolder from "@service/components/whitelist-holder"
1414
import { IS_ANDROID } from "@util/constant/environment"
1515
import { extractHostname, isBrowserUrl } from "@util/pattern"
1616
import { MILL_PER_HOUR, MILL_PER_MINUTE, MILL_PER_SECOND } from "@util/time"
1717
import MessageDispatcher from "./message-dispatcher"
1818

19-
const statDatabase: StatDatabase = new StatDatabase(chrome.storage.local)
20-
2119
export type BadgeLocation = {
2220
/**
2321
* The tab id of badge text show display with

src/background/icon-and-alias-collector.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
*/
77

88
import { getTab } from "@api/chrome/tab"
9-
import OptionDatabase from "@db/option-database"
9+
import optionDatabase from "@db/option-database"
1010
import siteService from "@service/site-service"
1111
import { IS_ANDROID, IS_CHROME, IS_SAFARI } from "@util/constant/environment"
1212
import { defaultStatistics } from "@util/constant/option"
1313
import { extractHostname, isBrowserUrl, isHomepage } from "@util/pattern"
1414
import { extractSiteName } from "@util/site"
1515

1616
const storage: chrome.storage.StorageArea = chrome.storage.local
17-
const optionDatabase = new OptionDatabase(storage)
1817

1918
let collectAliasEnabled = defaultStatistics().collectSiteName
2019
const setCollectAliasEnabled = (opt: timer.option.AllOption) => collectAliasEnabled = opt.collectSiteName

src/background/migrator/host-merge-initializer.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
* https://opensource.org/licenses/MIT
66
*/
77

8-
import MergeRuleDatabase from "@db/merge-rule-database"
8+
import mergeRuleDatabase from "@db/merge-rule-database"
99
import { type Migrator } from "./common"
1010

11-
const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local)
12-
1311
/**
1412
* v0.1.2
1513
*

src/background/migrator/local-file-initializer.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
* https://opensource.org/licenses/MIT
66
*/
77

8-
import MergeRuleDatabase from "@db/merge-rule-database"
8+
import mergeRuleDatabase from "@db/merge-rule-database"
99
import { t2Chrome } from "@i18n/chrome/t"
1010
import siteService from "@service/site-service"
1111
import { JSON_HOST, LOCAL_HOST_PATTERN, MERGED_HOST, PDF_HOST, PIC_HOST, TXT_HOST } from "@util/constant/remain-host"
1212
import { type Migrator } from "./common"
1313

14-
const mergeRuleDatabase = new MergeRuleDatabase(chrome.storage.local)
15-
1614
/**
1715
* Process the host of local files
1816
*

src/background/track-server.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getTab, listTabs, sendMsg2Tab } from "@api/chrome/tab"
22
import { getWindow } from "@api/chrome/window"
33
import optionHolder from "@service/components/option-holder"
4-
import itemService from "@service/item-service"
4+
import whitelistHolder from "@service/components/whitelist-holder"
5+
import itemService, { type ItemIncContext } from "@service/item-service"
56
import limitService from "@service/limit-service"
67
import periodService from "@service/period-service"
78
import { IS_ANDROID } from "@util/constant/environment"
@@ -10,11 +11,12 @@ import { formatTimeYMD, getStartOfDay, MILL_PER_DAY } from "@util/time"
1011
import badgeManager from "./badge-manager"
1112
import MessageDispatcher from "./message-dispatcher"
1213

13-
async function handleTime(host: string, url: string, dateRange: [number, number], tabId: number | undefined): Promise<number> {
14-
const [start, end] = dateRange
14+
async function handleTime(context: ItemIncContext, timeRange: [number, number], tabId: number | undefined): Promise<number> {
15+
const { host, url } = context
16+
const [start, end] = timeRange
1517
const focusTime = end - start
1618
// 1. Save async
17-
await itemService.addFocusTime(host, url, focusTime)
19+
await itemService.addFocusTime(context, focusTime)
1820
// 2. Process limit
1921
const { limited, reminder } = await limitService.addFocusTime(host, url, focusTime)
2022
// If time limited after this operation, send messages
@@ -28,15 +30,18 @@ async function handleTime(host: string, url: string, dateRange: [number, number]
2830

2931
async function handleTrackTimeEvent(event: timer.core.Event, sender: ChromeMessageSender): Promise<void> {
3032
const { url, start, end, ignoreTabCheck } = event
31-
const { id: tabId, windowId } = sender?.tab || {}
33+
const { id: tabId, windowId, groupId } = sender?.tab || {}
3234
if (!ignoreTabCheck) {
3335
if (await windowNotFocused(windowId)) return
3436
if (await tabNotActive(tabId)) return
3537
}
3638
const { protocol, host } = extractHostname(url) || {}
3739
const option = await optionHolder.get()
40+
3841
if (protocol === "file" && !option?.countLocalFiles) return
39-
await handleTime(host, url, [start, end], tabId)
42+
if (whitelistHolder.contains(host, url)) return
43+
44+
await handleTime({ host, url, groupId }, [start, end], tabId)
4045
if (tabId) {
4146
const winTabs = await listTabs({ active: true, windowId })
4247
const firstActiveTab = winTabs?.[0]
@@ -73,19 +78,21 @@ async function sendLimitedMessage(items: timer.limit.Item[]) {
7378
}
7479
}
7580

76-
async function handleVisit(host: string, url: string) {
77-
await itemService.increaseVisit(host, url)
81+
async function handleVisit(context: ItemIncContext) {
82+
await itemService.increaseVisit(context)
83+
const { host, url } = context
7884
const metLimits = await limitService.incVisit(host, url)
7985
// If time limited after this operation, send messages
8086
metLimits?.length && sendLimitedMessage(metLimits)
8187
}
8288

83-
async function handleIncVisitEvent(param: { host: string, url: string }): Promise<void> {
89+
async function handleIncVisitEvent(param: { host: string, url: string }, sender: ChromeMessageSender): Promise<void> {
8490
const { host, url } = param || {}
91+
const { groupId } = sender?.tab ?? {}
8592
const { protocol } = extractHostname(url) || {}
8693
const option = await optionHolder.get()
8794
if (protocol === "file" && !option.countLocalFiles) return
88-
await handleVisit(host, url)
95+
await handleVisit({ host, url, groupId })
8996
}
9097

9198
function splitRunTime(start: number, end: number): Record<string, number> {
@@ -108,14 +115,28 @@ async function handleTrackRunTimeEvent(event: timer.core.Event): Promise<void> {
108115
const realStart = Math.max(RUN_TIME_END_CACHE[host] ?? 0, start)
109116
const byDate = splitRunTime(realStart, end)
110117
if (!Object.keys(byDate).length) return
111-
await itemService.addRunTime(host, url, byDate)
118+
await itemService.addRunTime(host, byDate)
112119
RUN_TIME_END_CACHE[host] = Math.max(end, realStart)
113120
}
114121

122+
function handleTabGroupRemove(group: chrome.tabGroups.TabGroup) {
123+
itemService.batchDeleteGroupById(group.id)
124+
}
125+
126+
function handleTabGroupEnabled() {
127+
try {
128+
chrome.tabGroups.onRemoved.removeListener(handleTabGroupRemove)
129+
chrome.tabGroups.onRemoved.addListener(handleTabGroupRemove)
130+
} catch (e) {
131+
console.warn('failed to handle event: enableTabGroup', e)
132+
}
133+
}
134+
115135
export default function initTrackServer(messageDispatcher: MessageDispatcher) {
116136
messageDispatcher
117137
.register<timer.core.Event, void>('cs.trackTime', handleTrackTimeEvent)
118138
.register<timer.core.Event, void>('cs.trackRunTime', handleTrackRunTimeEvent)
119139
.register<{ host: string, url: string }, void>('cs.incVisitCount', handleIncVisitEvent)
120140
.register<string, timer.core.Result>('cs.getTodayInfo', host => itemService.getResult(host, new Date()))
141+
.register<void, void>('enableTabGroup', handleTabGroupEnabled)
121142
}

src/background/whitelist-menu-manager.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@
88
import { createContextMenu, updateContextMenu } from "@api/chrome/context-menu"
99
import { getRuntimeId } from "@api/chrome/runtime"
1010
import { getTab, onTabActivated, onTabUpdated } from "@api/chrome/tab"
11-
import WhitelistDatabase from "@db/whitelist-database"
11+
import db from "@db/whitelist-database"
1212
import { t2Chrome } from "@i18n/chrome/t"
1313
import { type ContextMenusMessage } from "@i18n/message/common/context-menus"
1414
import optionHolder from "@service/components/option-holder"
1515
import { IS_ANDROID } from "@util/constant/environment"
1616
import { extractHostname, isBrowserUrl } from "@util/pattern"
1717

18-
const db = new WhitelistDatabase(chrome.storage.local)
19-
2018
const menuId = '_timer_menu_item_' + getRuntimeId()
2119
let currentActiveId: number
2220

src/content-script/limit/modal/components/Footer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const _default = defineComponent(() => {
5252
<div class='footer-container'>
5353
<ElButton
5454
round
55-
icon={<Trend />}
55+
icon={Trend}
5656
type="success"
5757
onClick={() => sendMsg2Runtime('cs.openAnalysis')}
5858
>
@@ -62,14 +62,14 @@ const _default = defineComponent(() => {
6262
v-show={showDelay.value}
6363
type="primary"
6464
round
65-
icon={<Plus />}
65+
icon={Plus}
6666
onClick={() => handleMore5Minutes(rule.value, delayHandler)}
6767
>
6868
{t(msg => msg.modal.more5Minutes)}
6969
</ElButton>
7070
<ElButton
7171
round
72-
icon={<Timer />}
72+
icon={Timer}
7373
onClick={() => sendMsg2Runtime('cs.openLimit')}
7474
>
7575
{t(msg => msg.modal.ruleDetail)}

0 commit comments

Comments
 (0)