Skip to content

Commit

Permalink
Support import data of Web Activity Time Tracker (#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheepzh committed Jul 21, 2023
1 parent 13f7dd8 commit 70ef69c
Show file tree
Hide file tree
Showing 22 changed files with 726 additions and 176 deletions.
60 changes: 30 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,51 @@
},
"license": "MIT",
"devDependencies": {
"@crowdin/crowdin-api-client": "^1.22.1",
"@types/chrome": "0.0.224",
"@crowdin/crowdin-api-client": "^1.23.3",
"@types/chrome": "0.0.241",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/echarts": "^4.9.16",
"@types/echarts": "^4.9.18",
"@types/generate-json-webpack-plugin": "^0.3.4",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.3",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.2",
"@types/psl": "^1.1.0",
"@types/webpack": "^5.28.0",
"@types/webpack": "^5.28.1",
"@types/webpack-bundle-analyzer": "^4.6.0",
"babel-loader": "^9.1.2",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"eslint": "^8.36.0",
"css-loader": "^6.8.1",
"eslint": "^8.44.0",
"filemanager-webpack-plugin": "^8.0.0",
"generate-json-webpack-plugin": "^2.0.0",
"html-webpack-plugin": "^5.5.1",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"mini-css-extract-plugin": "^2.7.5",
"node-sass": "^8.0.0",
"sass-loader": "^13.2.1",
"style-loader": "^3.3.2",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"html-webpack-plugin": "^5.5.3",
"jest": "^29.6.1",
"jest-environment-jsdom": "^29.6.1",
"mini-css-extract-plugin": "^2.7.6",
"node-sass": "^9.0.0",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2",
"tslib": "^2.5.0",
"typescript": "5.0.2",
"tsconfig-paths": "^4.2.0",
"tslib": "^2.6.0",
"typescript": "5.1.6",
"url-loader": "^4.1.1",
"webpack": "^5.76.2",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.0.1"
"webpack": "^5.88.1",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.3.4",
"axios": "^1.4.0",
"clipboardy": "^3.0.0",
"countup.js": "^2.6.0",
"echarts": "^5.4.1",
"element-plus": "2.3.1",
"countup.js": "^2.7.0",
"echarts": "^5.4.2",
"element-plus": "2.3.7",
"psl": "^1.9.0",
"stream-browserify": "^3.0.0",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"engines": {
"node": ">=16"
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/common/editable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { Check, Close, Edit } from "@element-plus/icons-vue"
import { defineComponent, h, nextTick, watch } from "@vue/runtime-core"
import { defineComponent, h, nextTick, watch } from "vue"
import { ElButton, ElIcon, ElInput } from "element-plus"
import { Ref, ref, SetupContext } from "vue"

Expand Down
27 changes: 12 additions & 15 deletions src/app/components/data-manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,16 @@ import MemeryInfo from "./memory-info"
import ClearPanel from "./clear"
import './style'

export default defineComponent({
name: "DataManage",
setup() {
const memeryInfoRef: Ref = ref()
const queryData = () => memeryInfoRef?.value?.queryData()
return () => h(ContentContainer, {
class: 'data-manage-container'
}, () => h(ElRow, { gutter: 20 },
() => [
h(ElCol, { span: 8 }, () => h(MemeryInfo, { ref: memeryInfoRef })),
h(ElCol, { span: 11 }, () => h(ClearPanel, { onDataDelete: queryData })),
h(ElCol, { span: 5 }, () => h(Migration, { onImport: queryData })),
]
))
}
export default defineComponent(() => {
const memeryInfoRef: Ref = ref()
const queryData = () => memeryInfoRef?.value?.queryData()
return () => h(ContentContainer, {
class: 'data-manage-container'
}, () => h(ElRow, { gutter: 20 },
() => [
h(ElCol, { span: 8 }, () => h(MemeryInfo, { ref: memeryInfoRef })),
h(ElCol, { span: 11 }, () => h(ClearPanel, { onDataDelete: queryData })),
h(ElCol, { span: 5 }, () => h(Migration, { onImport: queryData })),
]
))
})
81 changes: 0 additions & 81 deletions src/app/components/data-manage/migration.ts

This file was deleted.

59 changes: 59 additions & 0 deletions src/app/components/data-manage/migration/import-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2021 Hengyang Zhang
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/

import { t } from "@app/locale"
import { Upload } from "@element-plus/icons-vue"
import Immigration from "@service/components/immigration"
import { deserialize } from "@util/file"
import { ElButton, ElLoading, ElMessage } from "element-plus"
import { Ref, defineComponent, h, ref } from "vue"

const immigration: Immigration = new Immigration()

async function handleFileSelected(fileInputRef: Ref<HTMLInputElement>, callback: () => void) {
const files: FileList | null = fileInputRef?.value?.files
if (!files || !files.length) {
return
}
const loading = ElLoading.service({ fullscreen: true })
const file: File = files[0]
const fileText = await file.text()
const data = deserialize(fileText)
if (!data) {
ElMessage.error(t(msg => msg.dataManage.importError))
}
await immigration.importData(data)
loading.close()
callback?.()
ElMessage.success(t(msg => msg.dataManage.migrated))
}

const _default = defineComponent({
emits: {
import: () => true
},
setup(_, ctx) {
const fileInputRef = ref<HTMLInputElement>()
return () => h(ElButton, {
size: 'large',
type: 'primary',
icon: Upload,
onClick: () => fileInputRef.value.click()
}, () => [
t(msg => msg.item.operation.importWholeData),
h('input', {
ref: fileInputRef,
type: 'file',
accept: '.json',
style: { display: 'none' },
onChange: () => handleFileSelected(fileInputRef, () => ctx.emit('import'))
})
])
}
})

export default _default
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { t } from "@app/locale"
import { Upload } from "@element-plus/icons-vue"
import { ElButton, ElDialog } from "element-plus"
import { Ref, defineComponent, h, ref } from "vue"
import Sop from "./sop"
import "./style"

const _default = defineComponent({
emits: {
import: () => true
},
setup(_) {
const dialogVisible: Ref<boolean> = ref(false)
const close = () => dialogVisible.value = false
return () => [
h(ElButton, {
size: 'large',
type: 'warning',
icon: Upload,
onClick: () => dialogVisible.value = true
}, () => t(msg => msg.item.operation.importOtherData)),
h(ElDialog, {
top: '10vh',
modelValue: dialogVisible.value,
title: t(msg => msg.item.operation.importOtherData),
width: '80%',
closeOnClickModal: false,
}, () => h(Sop, {
onCancel: close,
onImport: close,
}))
]
}
})

export default _default
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import StatDatabase from "@db/stat-database"
import { isNotZeroResult } from "@util/stat"

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

export type OtherExtension = "web_activity_time_tracker"

export type ActionType = 'overwrite' | 'accumulate'

export type ImportedRow = Required<timer.stat.RowKey> & Partial<timer.stat.Result> & {
exist?: timer.stat.Result
}

export type ImportedData = {
// Whether there is data for this dimension
[dimension in timer.stat.Dimension]?: boolean
} & {
rows: ImportedRow[]
}

/**
* Parse the content to rows
*
* @param type extension type
* @param file selected file
* @returns row data
*/
export async function parseFile(ext: OtherExtension, file: File): Promise<ImportedData> {
const text = await file.text()
if (ext === 'web_activity_time_tracker') {
const lines = text.split('\n').map(line => line.trim()).filter(line => !!line).splice(1)
const rows: ImportedRow[] = lines.map(line => {
const [host, date, seconds] = line.split(',').map(cell => cell.trim())
const [year, month, day] = date.split('/')
const realDate = `${year}${month.length == 2 ? month : '0' + month}${day.length == 2 ? day : '0' + day}`
return { host, date: realDate, focus: parseInt(seconds) * 1000 }
})
await doIfExist(rows, (row, exist) => row.exist = exist)
return { rows, focus: true }
} else {
return { rows: [] }
}
}

async function doIfExist<T extends timer.stat.RowKey>(items: T[], processor: (item: T, existVal: timer.stat.Result) => any): Promise<void> {
await Promise.all(items.map(async item => {
const { host, date } = item
const exist = await statDatabase.get(host, date)
isNotZeroResult(exist) && processor(item, exist)
}))
}

/**
* Import data
*/
export async function processImport(data: ImportedData, action: ActionType): Promise<void> {
if (action === 'overwrite') {
return processOverwrite(data)
} else {
return processAcc(data)
}
}

function processOverwrite(data: ImportedData): Promise<any> {
const { rows, focus, time } = data
return Promise.all(rows.map(async row => {
const { host, date } = row
const exist = await statDatabase.get(host, date)
focus && (exist.focus = row.focus || 0)
time && (exist.time = row.time || 0)
await statDatabase.forceUpdate({ host, date, ...exist })
}))
}

function processAcc(data: ImportedData): Promise<any> {
const { rows } = data
return Promise.all(rows.map(async row => {
const { host, date, focus = 0, time = 0 } = row
await statDatabase.accumulate(host, date, { focus, time })
}))
}
Loading

0 comments on commit 70ef69c

Please sign in to comment.