Skip to content

Commit df73190

Browse files
committed
feat: semi
1 parent 3fee055 commit df73190

File tree

17 files changed

+334
-31
lines changed

17 files changed

+334
-31
lines changed

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@
3131
"@crowdin/crowdin-api-client": "^1.52.0",
3232
"@emotion/babel-plugin": "^11.13.5",
3333
"@emotion/css": "^11.13.5",
34-
"@rsdoctor/rspack-plugin": "^1.5.0",
35-
"@rspack/cli": "^1.7.3",
36-
"@rspack/core": "^1.7.3",
37-
"@swc/core": "^1.15.10",
34+
"@rsdoctor/rspack-plugin": "^1.5.2",
35+
"@rspack/cli": "^1.7.5",
36+
"@rspack/core": "^1.7.5",
37+
"@swc/core": "^1.15.11",
3838
"@swc/jest": "^0.2.39",
39-
"@types/chrome": "0.1.35",
39+
"@types/chrome": "0.1.36",
4040
"@types/decompress": "^4.2.7",
4141
"@types/jest": "^30.0.0",
42-
"@types/node": "^25.0.9",
42+
"@types/node": "^25.2.2",
4343
"@types/punycode": "^2.1.4",
4444
"@vue/babel-plugin-jsx": "^2.0.1",
4545
"babel-loader": "^10.0.0",
46-
"commitlint": "^20.3.1",
47-
"css-loader": "^7.1.2",
46+
"commitlint": "^20.4.1",
47+
"css-loader": "^7.1.3",
4848
"decompress": "^4.2.1",
4949
"husky": "^9.1.7",
5050
"jest": "^30.2.0",
@@ -54,7 +54,7 @@
5454
"postcss": "^8.5.6",
5555
"postcss-loader": "^8.2.0",
5656
"postcss-rtlcss": "^5.7.1",
57-
"puppeteer": "^24.35.0",
57+
"puppeteer": "^24.37.2",
5858
"ts-loader": "^9.5.4",
5959
"ts-node": "^10.9.2",
6060
"tsconfig-paths": "^4.2.0",
@@ -63,10 +63,11 @@
6363
"dependencies": {
6464
"@element-plus/icons-vue": "^2.3.2",
6565
"echarts": "^6.0.0",
66-
"element-plus": "2.13.1",
66+
"element-plus": "2.13.2",
6767
"punycode": "^2.3.1",
68+
"typescript-guard": "^0.2.1",
6869
"vue": "^3.5.27",
69-
"vue-router": "^4.6.4"
70+
"vue-router": "^5.0.2"
7071
},
7172
"engines": {
7273
"node": ">=22"

rspack/rspack.common.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const generateJsonPlugins: RspackPluginInstance[] = []
1515

1616
const localeJsonFiles = Object.entries(i18nChrome)
1717
.map(([locale, message]) => new GenerateJsonPlugin(`_locales/${locale}/messages.json`, message))
18-
.map(plugin => plugin as unknown as RspackPluginInstance)
1918
generateJsonPlugins.push(...localeJsonFiles)
2019

2120
type EntryConfig = {

src/background/message-dispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class MessageDispatcher {
3333
const result = await handler(message.data, sender)
3434
return { code: 'success', data: result }
3535
} catch (error) {
36-
const msg = (error as Error)?.message ?? error?.toString?.()
36+
const msg = error instanceof Error ? error.message : error?.toString?.()
3737
return { code: 'fail', msg }
3838
}
3939
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copy from chrome.storage, but not exported
2+
type NoInferX<T> = T[][T extends any ? 0 : never]
3+
4+
const ALL_TABLES = ['stat'] as const
5+
6+
export type IndexedTable = typeof ALL_TABLES[number]
7+
8+
const DB_NAME = `tt4b_${chrome.runtime.id}`
9+
10+
const initDb = (): Promise<IDBDatabase> => {
11+
const factory = typeof window === 'undefined' ? self.indexedDB : window.indexedDB
12+
const request = factory.open(DB_NAME)
13+
return new Promise((resolve, reject) => {
14+
request.onsuccess = () => {
15+
const db = request.result
16+
const existingStores = Array.from(db.objectStoreNames)
17+
const missingStores = ALL_TABLES.filter(table => !existingStores.includes(table))
18+
if (!missingStores.length) {
19+
return resolve(db)
20+
}
21+
22+
const currentVersion = db.version
23+
db.close()
24+
25+
const upgradeRequest = factory.open(DB_NAME, currentVersion + 1)
26+
27+
upgradeRequest.onupgradeneeded = () => {
28+
const upgradeDb = upgradeRequest.result
29+
for (const table of missingStores) {
30+
if (!upgradeDb.objectStoreNames.contains(table)) {
31+
upgradeDb.createObjectStore(table)
32+
}
33+
}
34+
}
35+
36+
upgradeRequest.onsuccess = () => {
37+
console.log("IndexedDB upgraded")
38+
resolve(upgradeRequest.result)
39+
}
40+
41+
upgradeRequest.onerror = () => reject(upgradeRequest.error)
42+
}
43+
44+
request.onerror = (ev) => {
45+
console.error("IndexedDB initialized error: ", ev)
46+
reject(ev)
47+
}
48+
})
49+
}
50+
51+
function promiseRequest<T>(req: IDBRequest<T>): Promise<T> {
52+
return new Promise((resolve, reject) => {
53+
req.onsuccess = () => resolve(req.result)
54+
req.onerror = (ev) => {
55+
console.error("Failed to request indexed-db", ev, req.error)
56+
reject(req.error)
57+
}
58+
})
59+
}
60+
61+
function cvtDbKey2Str(dbKey: IDBValidKey): string {
62+
if (typeof dbKey === 'string') {
63+
return dbKey
64+
} else if (typeof dbKey === 'number') {
65+
return `${dbKey}`
66+
} else if (dbKey instanceof Date) {
67+
return `${dbKey.getTime()}`
68+
} else {
69+
return dbKey.toString()
70+
}
71+
}
72+
73+
export class IndexedDbStorage implements chrome.storage.StorageArea {
74+
private db: IDBDatabase | undefined
75+
76+
onChanged: chrome.events.Event<(changes: { [key: string]: chrome.storage.StorageChange }) => void> = {
77+
addListener: function (callback: (changes: { [key: string]: chrome.storage.StorageChange }) => void): void {
78+
throw new Error('Function not implemented.')
79+
},
80+
getRules: function (ruleIdentifiers: string[] | ArgCallback<chrome.events.Rule[]>, callback?: ArgCallback<chrome.events.Rule[]>): void {
81+
// not unsupported
82+
},
83+
hasListener: function (callback: (changes: { [key: string]: chrome.storage.StorageChange }) => void): boolean {
84+
return false
85+
},
86+
removeRules: function (ruleIdentifiers: string[] | undefined | ArgCallback<chrome.events.Rule[]>, callback?: ArgCallback<chrome.events.Rule[]>): void {
87+
// not unsupported
88+
},
89+
addRules: function (rules: chrome.events.Rule[], callback?: (rules: chrome.events.Rule[]) => void): void {
90+
// not unsupported
91+
},
92+
removeListener: function (callback: (changes: { [key: string]: chrome.storage.StorageChange }) => void): void {
93+
// not unsupported
94+
},
95+
hasListeners: function (): boolean {
96+
return false
97+
}
98+
}
99+
100+
constructor(private table: IndexedTable) { }
101+
102+
private async withTransaction<T>(operation: (store: IDBObjectStore) => Promise<T>, mode?: IDBTransactionMode): Promise<T> {
103+
if (!this.db) {
104+
(self as any)._db = this.db = await initDb()
105+
}
106+
const transaction = this.db.transaction([this.table], mode ?? 'readwrite')
107+
const store = transaction.objectStore(this.table)
108+
try {
109+
return await operation(store)
110+
} finally {
111+
transaction.commit()
112+
}
113+
}
114+
115+
async clear(callback?: NoArgCallback): Promise<void> {
116+
await this.withTransaction(async store => {
117+
const req = store.clear()
118+
await promiseRequest(req)
119+
})
120+
callback?.()
121+
}
122+
123+
async set<T = { [key: string]: any }>(items: Partial<T>, callback?: NoArgCallback): Promise<void> {
124+
await this.withTransaction(async store => {
125+
const promises: Promise<unknown>[] = []
126+
127+
for (const [key, value] of Object.entries(items)) {
128+
const promise = promiseRequest(store.put(value, key))
129+
promises.push(promise)
130+
}
131+
await Promise.all(promises)
132+
})
133+
134+
callback?.()
135+
}
136+
137+
async remove<T = { [key: string]: any }>(keys: keyof T | Array<keyof T>, callback?: () => void): Promise<void> {
138+
await this.withTransaction(async store => {
139+
keys = Array.isArray(keys) ? keys : [keys]
140+
await Promise.all(keys.map(key => typeof key !== 'symbol' && promiseRequest(store.delete(key))))
141+
})
142+
callback?.()
143+
}
144+
145+
async get<T = { [key: string]: unknown }>(
146+
keys: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null | undefined | ArgCallback<T>,
147+
callback?: ArgCallback<T>,
148+
): Promise<T> {
149+
if (typeof keys === 'function') {
150+
callback = keys
151+
keys = null
152+
}
153+
const data = await this.getData(keys)
154+
callback?.(data)
155+
return data
156+
}
157+
158+
private async getData<T = { [key: string]: unknown }>(keys?: NoInferX<keyof T> | Array<NoInferX<keyof T>> | Partial<NoInferX<T>> | null): Promise<T> {
159+
const result = {} as { [key: string]: unknown }
160+
161+
await this.withTransaction(async store => {
162+
const fill = async (key: keyof NoInferX<T>) => {
163+
if (typeof key === 'symbol') return
164+
const value = await promiseRequest<unknown>(store.get(key))
165+
result[cvtDbKey2Str(key)] = value
166+
}
167+
168+
if (keys == null) {
169+
const allKeys = await promiseRequest(store.getAllKeys())
170+
const allValues = await promiseRequest(store.getAll())
171+
allKeys.forEach((key, index) => result[cvtDbKey2Str(key)] = allValues[index])
172+
} else if (Array.isArray(keys)) {
173+
await Promise.all(keys.map(fill))
174+
} else if (typeof keys === 'object') {
175+
for (const key in keys) {
176+
await fill(key)
177+
}
178+
} else {
179+
await fill(keys)
180+
}
181+
}, 'readonly')
182+
183+
return result as T
184+
}
185+
186+
async setAccessLevel(accessOptions: { accessLevel: `${chrome.storage.AccessLevel}` }, callback?: NoArgCallback): Promise<void> {
187+
// no nothing
188+
console.info("Invoked setAccessLevel()", accessOptions)
189+
callback?.()
190+
}
191+
192+
async getKeys(callback?: ArgCallback<string[]>): Promise<string[]> {
193+
const keys = await this.withTransaction(async store => {
194+
const dbKeys = await promiseRequest(store.getAllKeys())
195+
return dbKeys.map(cvtDbKey2Str)
196+
}, 'readonly')
197+
callback?.(keys)
198+
return keys
199+
}
200+
201+
async getBytesInUse<T = { [key: string]: any }>(
202+
keys?: keyof T | Array<keyof T> | null | ArgCallback<number>,
203+
callback?: ArgCallback<number>
204+
): Promise<number> {
205+
[keys, callback] = typeof keys === 'function' ? [null, keys] : [keys, callback]
206+
const value = 0
207+
callback?.(value)
208+
return value
209+
}
210+
}

src/database/common/storage-promise.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class StoragePromise {
4444
/**
4545
* @since 0.5.0
4646
*/
47-
put(key: string, val: Object): Promise<void> {
47+
put(key: string, val: unknown): Promise<void> {
4848
return this.set({ [key]: val })
4949
}
5050

src/database/stat-database/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* https://opensource.org/licenses/MIT
66
*/
77

8+
import { IndexedDbStorage } from '@db/common/indexed-storage'
9+
import StoragePromise from '@db/common/storage-promise'
10+
import optionHolder from '@service/components/option-holder'
811
import { escapeRegExp } from "@util/pattern"
912
import { isNotZeroResult } from "@util/stat"
1013
import { formatTimeYMD } from "@util/time"
@@ -96,7 +99,28 @@ function migrate(exists: { [key: string]: timer.core.Result }, data: any): Recor
9699
return result
97100
}
98101

102+
103+
function convertStorage(type: timer.option.StorageType): StoragePromise | null {
104+
if (type === 'local') {
105+
return new StoragePromise(chrome.storage.local)
106+
} else if (type === 'indexed_db') {
107+
return new StoragePromise(new IndexedDbStorage('stat'))
108+
} else {
109+
return null
110+
}
111+
}
112+
99113
export class StatDatabase extends BaseDatabase {
114+
constructor() {
115+
super(chrome.storage.local)
116+
optionHolder.get().then(val => this.switchStorage(val))
117+
optionHolder.addChangeListener(val => this.switchStorage(val))
118+
}
119+
120+
private switchStorage(option: timer.option.TrackingOption) {
121+
const storage = convertStorage(option.storage)
122+
storage !== null && (this.storage = storage)
123+
}
100124

101125
async refresh(): Promise<{ [key: string]: unknown }> {
102126
const result = await this.storage.get()
@@ -309,6 +333,25 @@ export class StatDatabase extends BaseDatabase {
309333
const toSave = migrate(items, data)
310334
this.storage.set(toSave)
311335
}
336+
337+
/**
338+
* Since all the data to the target storage
339+
*
340+
* @since 3.8.0
341+
* @param targetType target storage type
342+
*/
343+
async migrateStorage(targetType: timer.option.StorageType) {
344+
const target = convertStorage(targetType)
345+
const original = this.storage
346+
if (target == null) return
347+
348+
const all = await this.refresh()
349+
await target.set(all)
350+
// remove original data
351+
try {
352+
await original.remove(Object.keys(all))
353+
} catch (ignored) { }
354+
}
312355
}
313356

314357
const statDatabase = new StatDatabase()

src/i18n/message/app/option-resource.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@
301301
"tabGroupsPermGrant": "This feature requires relevant permissions",
302302
"fileAccessDisabled": "Access to file URLs is currently not allowed. Please enable it on the manage page first",
303303
"weekStart": "The first day for each week {input}",
304-
"weekStartAsNormal": "As Normal"
304+
"weekStartAsNormal": "As Normal",
305+
"storage": "Store the tracking data in {input}"
305306
},
306307
"limit": {
307308
"prompt": "Prompt displayed when restricted {input}",

src/i18n/message/app/option.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type OptionMessage = {
5151
fileAccessDisabled: string
5252
weekStart: string
5353
weekStartAsNormal: string
54+
storage: string
5455
}
5556
limit: {
5657
prompt: string

src/pages/app/Layout/menu/Side.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ const _default = defineComponent(() => {
103103
>
104104
{collapsed.value
105105
? menus.flatMap(g => g.children).map(item => renderItem(item, router, curr))
106-
: menus.map(({ children, title }) => (
107-
<ElMenuItemGroup title={t(title)}>
108-
{children.map(item => renderItem(item, router, curr))}
109-
</ElMenuItemGroup>
110-
))}
106+
/// Not export title prop for group, so use `h` function
107+
: menus.map(({ children, title }) => h(
108+
ElMenuItemGroup,
109+
{ title: t(title) },
110+
() => children.map(item => renderItem(item, router, curr))),
111+
)}
111112
</ElMenu>
112113
</ElScrollbar>
113114
<Flex

0 commit comments

Comments
 (0)