diff --git a/.gitignore b/.gitignore index f2a1278..1d5f097 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ scripts/*.yml #Electron-builder output /dist_electron .serena/ -dist/* \ No newline at end of file +dist/* +test.js \ No newline at end of file diff --git a/package.json b/package.json index 5824a2d..b7340a5 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", + "@numairawan/video-duration": "^1.0.0", "@picgo/i18n": "^1.0.0", "@picgo/store": "^2.1.0", "axios": "^0.19.0", @@ -44,19 +45,21 @@ "lodash-id": "^0.14.0", "lowdb": "^1.0.0", "marked": "^7.0.4", + "mime-types": "^3.0.2", "mitt": "^3.0.1", "multer": "^1.4.5-lts.1", - "picgo": "^1.6.0", + "picgo": "^1.6.2", "qrcode.vue": "^3.3.3", "semver": "^7.7.3", "shell-path": "2.1.0", + "systeminformation": "^5.27.14", "tunnel": "^0.0.6", "uuid": "^9.0.0", "vue": "^3.3.4", "vue-router": "^4.2.2", "vue3-lazyload": "^0.3.6", "vue3-photo-preview": "^0.3.0", - "write-file-atomic": "^4.0.1" + "write-file-atomic": "^7.0.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.276.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37d6e97..a662772 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@element-plus/icons-vue': specifier: ^2.3.2 version: 2.3.2(vue@3.5.25(typescript@5.9.3)) + '@numairawan/video-duration': + specifier: ^1.0.0 + version: 1.0.0 '@picgo/i18n': specifier: ^1.0.0 version: 1.0.0 @@ -59,6 +62,9 @@ importers: marked: specifier: ^7.0.4 version: 7.0.5 + mime-types: + specifier: ^3.0.2 + version: 3.0.2 mitt: specifier: ^3.0.1 version: 3.0.1 @@ -66,8 +72,8 @@ importers: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.2 picgo: - specifier: ^1.6.0 - version: 1.6.0 + specifier: ^1.6.2 + version: 1.6.2 qrcode.vue: specifier: ^3.3.3 version: 3.6.0(vue@3.5.25(typescript@5.9.3)) @@ -77,6 +83,9 @@ importers: shell-path: specifier: 2.1.0 version: 2.1.0 + systeminformation: + specifier: ^5.27.14 + version: 5.27.14 tunnel: specifier: ^0.0.6 version: 0.0.6 @@ -96,8 +105,8 @@ importers: specifier: ^0.3.0 version: 0.3.0(vue@3.5.25(typescript@5.9.3)) write-file-atomic: - specifier: ^4.0.1 - version: 4.0.2 + specifier: ^7.0.0 + version: 7.0.0 devDependencies: '@aws-sdk/client-s3': specifier: ^3.276.0 @@ -1034,6 +1043,9 @@ packages: resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==} engines: {node: ^18.17.0 || >=20.5.0} + '@numairawan/video-duration@1.0.0': + resolution: {integrity: sha512-C819BxippZk2ajzfJt/S8kD5NJqAxuDZc1W4rqHVjzgoUI5Wjo7ehoH4dTIGesUEOxTobIXh6iOKAhhvGEDYpg==} + '@picgo/bump-version@2.0.0': resolution: {integrity: sha512-7ClH+1f0vNchLOzUq7OlYXic3dIDH154aC809BIwHFkVzoj4z4YPT5TrR1syICFDCJVixg8Tvlli0z891iO0vg==} engines: {node: '>=20.0.0'} @@ -1853,6 +1865,9 @@ packages: axios@0.27.2: resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + babel-polyfill@6.26.0: resolution: {integrity: sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==} @@ -3995,10 +4010,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.50.0: - resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==} - engines: {node: '>= 0.6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -4007,14 +4018,14 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.33: - resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -4454,8 +4465,8 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - picgo@1.6.0: - resolution: {integrity: sha512-BwWsXfx7LJEFBTCVs1QcEyhmXKqcgSemx0fZpp1Jus/LoEaybLQy8yEdEftKSf9c5BdOYfqqskQfZQ8L1p79hA==} + picgo@1.6.2: + resolution: {integrity: sha512-8o7ldUGHNywDX3keHeuiuSZZHt/hCyHepzzGA3Y3OdAZLIWdHI7NRjgUIQ2fsspQnkKaB4bm5xphoENhDiHuig==} engines: {node: '>= 20.0.0'} hasBin: true @@ -4591,6 +4602,9 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} @@ -5189,6 +5203,12 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + systeminformation@5.27.14: + resolution: {integrity: sha512-3DoNDYSZBLxBwaJtQGWNpq0fonga/VZ47HY1+7/G3YoIPaPz93Df6egSzzTKbEMmlzUpy3eQ0nR9REuYIycXGg==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + tailwindcss@3.4.18: resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} engines: {node: '>=14.0.0'} @@ -5619,6 +5639,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@7.0.0: + resolution: {integrity: sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==} + engines: {node: ^20.17.0 || >=22.9.0} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -6862,6 +6886,13 @@ snapshots: dependencies: semver: 7.7.3 + '@numairawan/video-duration@1.0.0': + dependencies: + axios: 1.13.2 + mime-types: 2.1.35 + transitivePeerDependencies: + - debug + '@picgo/bump-version@2.0.0(@types/node@20.19.26)(typescript@5.9.3)': dependencies: '@commitlint/cli': 7.6.1 @@ -7895,6 +7926,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-polyfill@6.26.0: dependencies: babel-runtime: 6.26.0 @@ -9415,7 +9454,7 @@ snapshots: combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 hasown: 2.0.2 - mime-types: 2.1.33 + mime-types: 2.1.35 formstream@1.5.2: dependencies: @@ -10431,20 +10470,18 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.50.0: {} - mime-db@1.52.0: {} mime-db@1.54.0: {} - mime-types@2.1.33: - dependencies: - mime-db: 1.50.0 - mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mime@2.6.0: {} mimic-fn@1.2.0: {} @@ -10862,7 +10899,7 @@ snapshots: pend@1.2.0: {} - picgo@1.6.0: + picgo@1.6.2: dependencies: '@picgo/i18n': 1.0.0 '@picgo/store': 2.1.0 @@ -10883,7 +10920,7 @@ snapshots: js-yaml: 4.1.1 lodash: 4.17.21 md5: 2.3.0 - mime-types: 2.1.33 + mime-types: 3.0.2 minimatch: 3.1.2 minimist: 1.2.8 qiniu: 7.14.0 @@ -10996,6 +11033,8 @@ snapshots: proto-list@1.2.4: {} + proxy-from-env@1.1.0: {} + pseudomap@1.0.2: {} pump@3.0.3: @@ -11672,6 +11711,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + systeminformation@5.27.14: {} + tailwindcss@3.4.18: dependencies: '@alloc/quick-lru': 5.2.0 @@ -12138,6 +12179,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + write-file-atomic@7.0.0: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + xml-name-validator@4.0.0: {} xmlbuilder@15.1.1: {} diff --git a/src/main/apis/app/remoteNotice/index.ts b/src/main/apis/app/remoteNotice/index.ts index 9a7db6b..7abc571 100644 --- a/src/main/apis/app/remoteNotice/index.ts +++ b/src/main/apis/app/remoteNotice/index.ts @@ -10,14 +10,13 @@ import axios from 'axios' import windowManager from '../window/windowManager' import { showNotification } from '~/main/utils/common' import { isDev } from '~/universal/utils/common' +import { STORE_PATH } from '~/main/utils/env' // for test -const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://picgo-1251750343.cos.accelerate.myqcloud.com/remote-notice.yml' +const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://release.picgo.app/remote-notice.yml' const REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'picgo-remote-notice.json' -const STORE_PATH = app.getPath('userData') - const REMOTE_NOTICE_LOCAL_STORAGE_PATH = path.join(STORE_PATH, REMOTE_NOTICE_LOCAL_STORAGE_FILE) class RemoteNoticeHandler { diff --git a/src/main/apis/app/uploader/apis.ts b/src/main/apis/app/uploader/apis.ts index 77b2509..b58e438 100644 --- a/src/main/apis/app/uploader/apis.ts +++ b/src/main/apis/app/uploader/apis.ts @@ -58,7 +58,7 @@ export const uploadClipboardFiles = async (): Promise => { } } -export const uploadChoosedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise => { +export const uploadSelectedFiles = async (webContents: WebContents, files: IFileWithPath[]): Promise => { const input = files.map(item => item.path) const imgs = await uploader.setWebContents(webContents).upload(input) const result = [] diff --git a/src/main/apis/app/uploader/index.ts b/src/main/apis/app/uploader/index.ts index 9b33d6e..82034fc 100644 --- a/src/main/apis/app/uploader/index.ts +++ b/src/main/apis/app/uploader/index.ts @@ -11,9 +11,9 @@ import db from '~/main/apis/core/datastore' import windowManager from 'apis/app/window/windowManager' import { IWindowList } from '#/types/enum' import util from 'util' -import { IPicGo } from 'picgo' -import { showNotification, calcDurationRange, getClipboardFilePathList } from '~/main/utils/common' -import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME, TALKING_DATA_EVENT } from '~/universal/events/constants' +import type { IPicGo } from 'picgo' +import { showNotification, getClipboardFilePathList } from '~/main/utils/common' +import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '~/universal/events/constants' import logger from '@core/picgo/logger' import { T } from '~/main/i18n' import fse from 'fs-extra' @@ -23,6 +23,7 @@ import writeFile from 'write-file-atomic' import { CLIPBOARD_IMAGE_FOLDER } from '~/universal/utils/static' import { cleanupFormUploaderFiles } from '~/main/utils/cleanupFormUploaderFiles' import { IpcMainEvent } from 'electron/main' +import { dataReportManager } from '~/main/utils/dataReport' const waitForRename = (window: BrowserWindow, id: number): Promise => { return new Promise((resolve) => { @@ -39,20 +40,6 @@ const waitForRename = (window: BrowserWindow, id: number): Promise }) } -const handleTalkingData = (webContents: WebContents, options: IAnalyticsData) => { - const data: ITalkingDataOptions = { - EventId: 'upload', - Label: options.type, - MapKv: { - by: options.fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件 - count: options.count, // 上传的数量 - duration: calcDurationRange(options.duration || 0), // 上传耗时 - type: options.type - } - } - webContents.send(TALKING_DATA_EVENT, data) -} - class Uploader { private webContents: WebContents | null = null // private uploading: boolean = false @@ -155,12 +142,11 @@ class Uploader { const output = await picgo.upload(img) if (Array.isArray(output) && output.some((item: ImgInfo) => item.imgUrl)) { if (this.webContents) { - handleTalkingData(this.webContents, { + dataReportManager.reportUploadData(this.webContents, { fromClipboard: !img, - type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms', - count: img ? img.length : 1, - duration: Date.now() - startTime - } as IAnalyticsData) + duration: Date.now() - startTime, + outputList: output + }); } return output.filter(item => item.imgUrl) } else { diff --git a/src/main/apis/core/bus/apis.ts b/src/main/apis/core/bus/apis.ts index 5a2bff6..4180f57 100644 --- a/src/main/apis/core/bus/apis.ts +++ b/src/main/apis/core/bus/apis.ts @@ -5,7 +5,7 @@ import { UPLOAD_WITH_CLIPBOARD_FILES, UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, GET_WINDOW_ID, - GET_WINDOW_ID_REPONSE, + GET_WINDOW_ID_RESPONSE, GET_SETTING_WINDOW_ID, GET_SETTING_WINDOW_ID_RESPONSE } from './constants' @@ -56,7 +56,7 @@ export const uploadWithFiles = (pathList: IFileWithPath[]): Promise<{ // miniWindow or settingWindow or trayWindow export const getWindowId = (): Promise => { return new Promise((resolve) => { - bus.once(GET_WINDOW_ID_REPONSE, (id: number) => { + bus.once(GET_WINDOW_ID_RESPONSE, (id: number) => { resolve(id) }) bus.emit(GET_WINDOW_ID) diff --git a/src/main/apis/core/bus/constants.ts b/src/main/apis/core/bus/constants.ts index 076c299..e5a3d3e 100644 --- a/src/main/apis/core/bus/constants.ts +++ b/src/main/apis/core/bus/constants.ts @@ -1,5 +1,5 @@ export const GET_WINDOW_ID = 'GET_WINDOW_ID' // get a current window -export const GET_WINDOW_ID_REPONSE = 'GET_WINDOW_ID_REPONSE' +export const GET_WINDOW_ID_RESPONSE = 'GET_WINDOW_ID_RESPONSE' export const GET_SETTING_WINDOW_ID = 'GET_SETTING_WINDOW_ID' // get setting window export const GET_SETTING_WINDOW_ID_RESPONSE = 'GET_SETTING_WINDOW_ID_RESPONSE' export const UPLOAD_WITH_FILES = 'UPLOAD_WITH_FILES' diff --git a/src/main/apis/core/datastore/dbChecker.ts b/src/main/apis/core/datastore/dbChecker.ts index ab91368..df89e94 100644 --- a/src/main/apis/core/datastore/dbChecker.ts +++ b/src/main/apis/core/datastore/dbChecker.ts @@ -1,12 +1,11 @@ import fs from 'fs-extra' import writeFile from 'write-file-atomic' import path from 'path' -import { app as APP } from 'electron' import { getLogger } from '@core/utils/localLogger' import dayjs from 'dayjs' import { T } from '~/main/i18n' import { FORM_IMAGE_FOLDER } from '~/universal/utils/static' -const STORE_PATH = APP.getPath('userData') +import { STORE_PATH } from '~/main/utils/env' const configFilePath = path.join(STORE_PATH, 'data.json') const configFileBackupPath = path.join(STORE_PATH, 'data.bak.json') export const defaultConfigPath = configFilePath diff --git a/src/main/events/busEventList.ts b/src/main/events/busEventList.ts index cb25eb1..a341c23 100644 --- a/src/main/events/busEventList.ts +++ b/src/main/events/busEventList.ts @@ -1,7 +1,7 @@ import bus from '@core/bus' import { uploadClipboardFiles, - uploadChoosedFiles + uploadSelectedFiles } from 'apis/app/uploader/apis' import { createMenu @@ -14,7 +14,7 @@ import { UPLOAD_WITH_CLIPBOARD_FILES, UPLOAD_WITH_CLIPBOARD_FILES_RESPONSE, GET_WINDOW_ID, - GET_WINDOW_ID_REPONSE, + GET_WINDOW_ID_RESPONSE, GET_SETTING_WINDOW_ID, GET_SETTING_WINDOW_ID_RESPONSE, CREATE_APP_MENU @@ -39,13 +39,13 @@ async function busCallUploadClipboardFiles () { async function busCallUploadFiles (pathList: IFileWithPath[]) { const win = windowManager.getAvailableWindow() - const urls = await uploadChoosedFiles(win.webContents, pathList) + const urls = await uploadSelectedFiles(win.webContents, pathList) bus.emit(UPLOAD_WITH_FILES_RESPONSE, urls) } function busCallGetWindowId () { const win = windowManager.getAvailableWindow() - bus.emit(GET_WINDOW_ID_REPONSE, win.id) + bus.emit(GET_WINDOW_ID_RESPONSE, win.id) } function busCallGetSettingWindowId () { diff --git a/src/main/events/ipcList.ts b/src/main/events/ipcList.ts index 75a3063..58c0e2d 100644 --- a/src/main/events/ipcList.ts +++ b/src/main/events/ipcList.ts @@ -31,15 +31,14 @@ import { } from '#/events/constants' import { uploadClipboardFiles, - uploadChoosedFiles + uploadSelectedFiles } from '~/main/apis/app/uploader/apis' import picgoCoreIPC from './picgoCoreIPC' import { handleCopyUrl } from '~/main/utils/common' import { buildMainPageMenu, buildMiniPageMenu, buildPluginPageMenu, buildPicBedListMenu } from './remotes/menu' import path from 'path' import { T } from '~/main/i18n' - -const STORE_PATH = app.getPath('userData') +import { STORE_PATH } from '~/main/utils/env' export default { listen () { @@ -74,7 +73,7 @@ export default { }) ipcMain.on('uploadChoosedFiles', async (evt: IpcMainEvent, files: IFileWithPath[]) => { - return uploadChoosedFiles(evt.sender, files) + return uploadSelectedFiles(evt.sender, files) }) ipcMain.on('updateShortKey', (evt: IpcMainEvent, item: IShortKeyConfig, oldKey: string, from: string) => { diff --git a/src/main/lifeCycle/index.ts b/src/main/lifeCycle/index.ts index d2fde82..7c47bd5 100644 --- a/src/main/lifeCycle/index.ts +++ b/src/main/lifeCycle/index.ts @@ -16,7 +16,7 @@ import { migrateGalleryFromVersion230 } from '~/main/migrate' import { - uploadChoosedFiles, + uploadSelectedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis' import { @@ -48,7 +48,7 @@ const handleStartUpFiles = (argv: string[], cwd: string) => { } else { logger.info('cli -> uploading files from cli', ...files.map(item => item.path)) const win = windowManager.getAvailableWindow() - uploadChoosedFiles(win.webContents, files) + uploadSelectedFiles(win.webContents, files) } return true } else { diff --git a/src/main/server/routerManager.ts b/src/main/server/routerManager.ts index 5d58922..3af1750 100644 --- a/src/main/server/routerManager.ts +++ b/src/main/server/routerManager.ts @@ -4,7 +4,7 @@ import { } from './utils' import logger from '@core/picgo/logger' import windowManager from 'apis/app/window/windowManager' -import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis' +import { uploadSelectedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis' import path from 'path' import { dbPathDir } from 'apis/core/datastore/dbChecker' const STORE_PATH = dbPathDir() @@ -51,7 +51,7 @@ router.post('/upload', async ({ } }) const win = windowManager.getAvailableWindow() - const res = await uploadChoosedFiles(win.webContents, pathList) + const res = await uploadSelectedFiles(win.webContents, pathList) logger.info('[PicGo Server] upload result', res.join(' ; ')) if (res.length) { handleResponse({ diff --git a/src/main/utils/common.ts b/src/main/utils/common.ts index caed1b8..d3fdf97 100644 --- a/src/main/utils/common.ts +++ b/src/main/utils/common.ts @@ -3,6 +3,7 @@ import db from '~/main/apis/core/datastore' import { clipboard, Notification, dialog } from 'electron' import { handleUrlEncode } from '~/universal/utils/common' import { readClipboardFilePaths } from 'clip-filepaths' +import crypto from 'node:crypto' export const handleCopyUrl = (str: string): void => { if (db.get('settings.autoCopyUrl') !== false) { @@ -54,7 +55,7 @@ export const showMessageBox = (options: any) => { }) } -export const calcDurationRange = (duration: number) => { +export const calcUploadProcessDurationRange = (duration: number) => { if (duration < 1000) { return 500 } else if (duration < 1500) { @@ -78,6 +79,44 @@ export const calcDurationRange = (duration: number) => { return 100000 } +// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ... +export const calcUploadBigFileSizeRange = (fileSizeMB: number) => { + if (fileSizeMB < 10) { + // 3.2 -> 3, 3.6 -> 4 + const result = Math.round(fileSizeMB); + return result === 0 && fileSizeMB > 0 ? 1 : result; + } + else if (fileSizeMB < 100) { + // 13 -> 1.3 -> 1 -> 10 + // 17 -> 1.7 -> 2 -> 20 + return Math.round(fileSizeMB / 10) * 10; + } + else { + // 135 -> 1.35 -> 1 -> 100 + // 160 -> 1.60 -> 2 -> 200 + return Math.round(fileSizeMB / 100) * 100; + } +} + +// 1 2 3 4 5 6 7 8 9 10 20 30 40 50 60 70 80 90 100 200 300 ... +export const calcVideoDurationRange = (durationSec: number) => { + if (durationSec < 10) { + // 3.2 -> 3, 3.6 -> 4 + const result = Math.round(durationSec); + return result === 0 && durationSec > 0 ? 1 : result; + } + else if (durationSec < 100) { + // 13 -> 1.3 -> 1 -> 10 + // 17 -> 1.7 -> 2 -> 20 + return Math.round(durationSec / 10) * 10; + } + else { + // 135 -> 1.35 -> 1 -> 100 + // 160 -> 1.60 -> 2 -> 200 + return Math.round(durationSec / 100) * 100; + } +} + /** * macOS public.file-url will get encoded file path, * so we need to decode it @@ -137,3 +176,7 @@ export const getHost = (url: string = '') => { export const removeProtocolAndSuffix = (url: string = '') => { return url.replace(/^(https?:\/\/)?/, '').replace(/\/$/, '') } + +export const md5 = (str: string): string => { + return crypto.createHash('md5').update(str).digest('hex') +} diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts new file mode 100644 index 0000000..f0259d9 --- /dev/null +++ b/src/main/utils/constants.ts @@ -0,0 +1,2 @@ +export const MB = 1024 * 1024 +export const SECOND = 1000 \ No newline at end of file diff --git a/src/main/utils/dataReport.ts b/src/main/utils/dataReport.ts new file mode 100644 index 0000000..c3f7788 --- /dev/null +++ b/src/main/utils/dataReport.ts @@ -0,0 +1,91 @@ +import { ipcMain, IpcMainEvent, type WebContents } from "electron"; +import { deviceIdManager } from "./deviceId"; +import { REGISTER_DEVICE_ID, TALKING_DATA_EVENT } from "~/universal/events/constants"; +import type { IImgInfo } from "picgo"; +import { calcUploadBigFileSizeRange, calcUploadProcessDurationRange, calcVideoDurationRange } from "./common"; +import { app } from "electron/main"; +import db from "~/main/apis/core/datastore"; +import fs from 'fs-extra' +// @ts-ignore +import { videoDuration } from "@numairawan/video-duration"; +import { MB, SECOND } from "./constants"; + +export interface IReportUploadDataOptions { + fromClipboard: boolean; + duration: number; + outputList: IImgInfo[]; +} + +class DataReportManager { + private deviceId: string | null = null; + private hasRegisterDeviceID: boolean = false; + constructor () { + this.init() + this.handleRegisterDeviceID() + } + private handleRegisterDeviceID() { + ipcMain.once(REGISTER_DEVICE_ID, (_evt: IpcMainEvent) => { + this.hasRegisterDeviceID = true; + console.log('Device ID registered'); + }); + } + private async init () { + if (this.deviceId) return; + this.deviceId = await deviceIdManager.getId() + } + public async reportUploadData(webContents: WebContents, options: IReportUploadDataOptions) { + await this.init(); + await this.registerDeviceID(webContents); + const { fromClipboard, duration, outputList } = options; + const fileList = outputList.map(item => { + return { + fileName: item.fileName, + filePath: item.filePath, + mimeType: item.mimeType + } + }) + const uploadEventData: ITalkingDataOptions = { + EventId: 'upload', + Label: '', + MapKv: { + by: fromClipboard ? 'clipboard' : 'files', // 上传剪贴板图片还是选择的文文件 + count: fileList.length, // 上传的数量 + duration: calcUploadProcessDurationRange(duration || 0), // 上传耗时 + type: db.get('picBed.uploader') || db.get('picBed.current') || 'smms', + deviceId: this.deviceId, + version: app.getVersion() + } + } + webContents.send(TALKING_DATA_EVENT, uploadEventData) + fileList.forEach(async file => { + if (file?.mimeType?.startsWith('video/') && file.filePath) { + const [stats, metadata] = await Promise.all([ + fs.stat(file.filePath), + videoDuration(file.filePath) + ]); + // report video duration and size range + const sizeMB = stats.size / MB; + const durationSec = (metadata.ms / SECOND) || 0; + const videoEventData: ITalkingDataOptions = { + EventId: 'upload_video', + Label: '', + MapKv: { + deviceId: this.deviceId, + sizeRange: calcUploadBigFileSizeRange(sizeMB), + durationRange: calcVideoDurationRange(durationSec), + version: app.getVersion() + } + }; + webContents.send(TALKING_DATA_EVENT, videoEventData); + } + }); + } + + public async registerDeviceID(webContents: WebContents) { + if (this.hasRegisterDeviceID) return; + await this.init(); + webContents.send(REGISTER_DEVICE_ID, this.deviceId); + } +} + +export const dataReportManager = new DataReportManager() diff --git a/src/main/utils/deviceId.ts b/src/main/utils/deviceId.ts new file mode 100644 index 0000000..deedf6f --- /dev/null +++ b/src/main/utils/deviceId.ts @@ -0,0 +1,62 @@ +import { networkInterfaceDefault } from 'systeminformation' +import { md5 } from './common' +import writeFile from 'write-file-atomic' +import { DEVICE_ID_PATH } from './env' +import fs from 'fs-extra' + +class DeviceIdManager { + private deviceId: string | null = null + + constructor() { + this.init() + } + + private async init() { + await this.loadDeviceId() + } + + private async getDeviceIdWithFallback(): Promise { + try { + if (fs.existsSync(DEVICE_ID_PATH)) { + const deviceId = await fs.readFile(DEVICE_ID_PATH, 'utf-8') + if (deviceId && deviceId?.trim().length > 0) { + return deviceId.trim() + } + } + const netInterfaceMac = await networkInterfaceDefault() + if (netInterfaceMac) { + return md5(netInterfaceMac) + } else { + // random fallback + return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`) + } + } catch (error) { + // random fallback + return md5(`${new Date().getTime()}-${Math.random().toString(36).substring(2, 15)}`) + } + } + + private async saveDeviceId(id: string): Promise { + try { + await writeFile(DEVICE_ID_PATH, id, { encoding: 'utf-8' }) + } catch (error) { + console.error('Failed to save device ID:', error) + } + } + + private async loadDeviceId(): Promise { + this.deviceId = await this.getDeviceIdWithFallback() + await this.saveDeviceId(this.deviceId) + return this.deviceId + } + + public async getId(): Promise { + if (this.deviceId) { + return this.deviceId + } + this.deviceId = await this.loadDeviceId() + return this.deviceId + } +} + +export const deviceIdManager = new DeviceIdManager() diff --git a/src/main/utils/env.ts b/src/main/utils/env.ts index 6d3a6d7..20396b4 100644 --- a/src/main/utils/env.ts +++ b/src/main/utils/env.ts @@ -29,3 +29,7 @@ export const buildRendererUrl = (hash?: string) => { } export const getStaticPath = () => process.env.STATIC_PATH || defaultStaticPath + +// paths +export const STORE_PATH = app.getPath('userData') +export const DEVICE_ID_PATH = path.join(STORE_PATH, 'picgo-device-id') diff --git a/src/renderer/main.ts b/src/renderer/main.ts index 3bcb2fc..829d516 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -31,7 +31,6 @@ app.config.globalProperties.$builtInPicBed = [ 'aliyun', 'github' ] -app.config.unwrapInjectedRef = true app.config.globalProperties.$$db = db app.config.globalProperties.$http = axios diff --git a/src/renderer/pages/TrayPage.vue b/src/renderer/pages/TrayPage.vue index d94202e..a4dc230 100644 --- a/src/renderer/pages/TrayPage.vue +++ b/src/renderer/pages/TrayPage.vue @@ -66,7 +66,7 @@ import { reactive, ref, onBeforeUnmount, onBeforeMount } from 'vue' import { ipcRenderer } from 'electron' import $$db from '@/utils/db' import { T as $T } from '@/i18n/index' -import { IResult } from '@picgo/store/dist/types' +import type { IResult } from '@picgo/store/dist/types' import { PASTE_TEXT, OPEN_WINDOW } from '#/events/constants' import { IWindowList } from '#/types/enum' import { sendToMain } from '@/utils/dataSender' diff --git a/src/renderer/utils/analytics.ts b/src/renderer/utils/analytics.ts index be60cb7..6da3fd3 100644 --- a/src/renderer/utils/analytics.ts +++ b/src/renderer/utils/analytics.ts @@ -1,5 +1,6 @@ import { - TALKING_DATA_APPID, TALKING_DATA_EVENT + REGISTER_DEVICE_ID, + TALKING_DATA_APPID, TALKING_DATA_DEVICE_ID_EVENT, TALKING_DATA_EVENT } from '~/universal/events/constants' import pkg from 'root/package.json' import { ipcRenderer } from 'electron' @@ -10,7 +11,7 @@ export const initTalkingData = () => { setTimeout(() => { const talkingDataScript = document.createElement('script') - talkingDataScript.src = `http://sdk.talkingdata.com/app/h5/v1?appid=${TALKING_DATA_APPID}&vn=${version}&vc=${version}` + talkingDataScript.src = `https://jic.talkingdata.com/app/h5/v1?appid=${TALKING_DATA_APPID}&vn=${version}&vc=${version}` const head = document.getElementsByTagName('head')[0] head.appendChild(talkingDataScript) @@ -20,3 +21,16 @@ export const initTalkingData = () => { ipcRenderer.on(TALKING_DATA_EVENT, (_, data: ITalkingDataOptions) => { handleTalkingDataEvent(data) }) +// 0:ANONYMOUS,匿名账号; +// 1:REGISTERED,自有帐户显性注册; +ipcRenderer.on(TALKING_DATA_DEVICE_ID_EVENT, (_, deviceId: string) => { + window.TDAPP.register({ + profileId: deviceId, + profileType: 1 + }) + window.TDAPP.login({ + profileId: deviceId, + profileType: 1 + }) + ipcRenderer.send(REGISTER_DEVICE_ID) +}); diff --git a/src/universal/events/constants.ts b/src/universal/events/constants.ts index 4bbd8ae..12e07a4 100644 --- a/src/universal/events/constants.ts +++ b/src/universal/events/constants.ts @@ -3,6 +3,7 @@ export const SHOW_INPUT_BOX_RESPONSE = 'SHOW_INPUT_BOX_RESPONSE' export const TOGGLE_SHORTKEY_MODIFIED_MODE = 'TOGGLE_SHORTKEY_MODIFIED_MODE' export const TALKING_DATA_APPID = '7E6832BCE3F1438696579E541DFEBFDA' export const TALKING_DATA_EVENT = 'TALKING_DATA_EVENT' +export const TALKING_DATA_DEVICE_ID_EVENT = 'TALKING_DATA_DEVICE_ID_EVENT' export const SHOW_PRIVACY_MESSAGE = 'SHOW_PRIVACY_MESSAGE' export const PICGO_SAVE_CONFIG = 'PICGO_SAVE_CONFIG' export const PICGO_GET_CONFIG = 'PICGO_GET_CONFIG' @@ -37,6 +38,7 @@ export const OPEN_WINDOW = 'OPEN_WINDOW' export const GET_PICBEDS = 'GET_PICBEDS' export const RPC_ACTIONS = 'RPC_ACTIONS' export const GET_PICBED_CONFIG = 'GET_PICBED_CONFIG' +export const REGISTER_DEVICE_ID = 'REGISTER_DEVICE_ID' // i18n export const GET_CURRENT_LANGUAGE = 'GET_CURRENT_LANGUAGE' export const GET_LANGUAGE_LIST = 'GET_LANGUAGE_LIST' diff --git a/src/universal/types/shims-tsx.d.ts b/src/universal/types/shims-tsx.d.ts index ab8df23..b344542 100644 --- a/src/universal/types/shims-tsx.d.ts +++ b/src/universal/types/shims-tsx.d.ts @@ -19,6 +19,14 @@ declare global { electronApi: ElectronApi TDAPP: { onEvent: (EventId: string, Label?: string, MapKv?: IStringKeyMap) => void + register: (opt: { + profileId: string, + profileType: number, + }) => void + login: (opt: { + profileId: string, + profileType: number, + }) => void } } }