diff --git a/src/main/apis/app/remoteNotice/index.ts b/src/main/apis/app/remoteNotice/index.ts new file mode 100644 index 0000000..9a7db6b --- /dev/null +++ b/src/main/apis/app/remoteNotice/index.ts @@ -0,0 +1,199 @@ +// get notice from remote +// such as some notices for users; some updates for users +import fs from 'fs-extra' +import { app, clipboard, dialog, shell } from 'electron' +import { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum' +import { lte, gte } from 'semver' +import path from 'path' + +import axios from 'axios' +import windowManager from '../window/windowManager' +import { showNotification } from '~/main/utils/common' +import { isDev } from '~/universal/utils/common' + +// 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_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 { + private remoteNotice: IRemoteNotice | null = null + private remoteNoticeLocalCountStorage: IRemoteNoticeLocalCountStorage | null = null + + async init () { + this.remoteNotice = await this.getRemoteNoticeInfo() + this.initLocalCountStorage() + } + + private initLocalCountStorage () { + const localCountStorage = {} + if (!fs.existsSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH)) { + fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify({})) + } + try { + const localCountStorage: IRemoteNoticeLocalCountStorage = fs.readJSONSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, 'utf8') + this.remoteNoticeLocalCountStorage = localCountStorage + } catch (e) { + console.log(e) + this.remoteNoticeLocalCountStorage = localCountStorage + } + } + + private saveLocalCountStorage (newData?: IRemoteNoticeLocalCountStorage) { + if (newData) { + this.remoteNoticeLocalCountStorage = newData + } + fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify(this.remoteNoticeLocalCountStorage)) + } + + private async getRemoteNoticeInfo (): Promise { + try { + const noticeInfo = await axios({ + method: 'get', + url: REMOTE_NOTICE_URL, + responseType: 'json' + }).then(res => res.data) as IRemoteNotice + return noticeInfo + } catch { + return null + } + } + + /** + * if the notice is not shown or is always shown, then show the notice + * @param action + */ + private checkActionCount (action: IRemoteNoticeAction) { + try { + if (!this.remoteNoticeLocalCountStorage) { + return true + } + const actionCount = this.remoteNoticeLocalCountStorage[action.id] + if (actionCount === undefined) { + if (action.triggerCount === IRemoteNoticeTriggerCount.ALWAYS) { + this.remoteNoticeLocalCountStorage[action.id] = 1 // if always, count number + } else { + this.remoteNoticeLocalCountStorage[action.id] = true + } + return true + } else { + // here is the count of action + // if not always show, then can't show + if (action.triggerCount !== IRemoteNoticeTriggerCount.ALWAYS) { + return false + } else { + const preCount = this.remoteNoticeLocalCountStorage[action.id] + if (typeof preCount !== 'number') { + this.remoteNoticeLocalCountStorage[action.id] = true + return true + } else { + this.remoteNoticeLocalCountStorage[action.id] = preCount + 1 + } + return true + } + } + } finally { + this.saveLocalCountStorage() + } + } + + private async doActions (actions: IRemoteNoticeAction[]) { + for (const action of actions) { + if (this.checkActionCount(action)) { + switch (action.type) { + case IRemoteNoticeActionType.SHOW_DIALOG: { + // SHOW DIALOG + const currentWindow = windowManager.getAvailableWindow() + dialog.showOpenDialog(currentWindow, action.data?.options) + break + } + case IRemoteNoticeActionType.SHOW_NOTICE: + showNotification({ + title: action.data?.title || '', + body: action.data?.content || '', + clickToCopy: !!action.data?.copyToClipboard, + copyContent: action.data?.copyToClipboard || '', + clickFn () { + if (action.data?.url) { + shell.openExternal(action.data.url) + } + } + }) + break + case IRemoteNoticeActionType.OPEN_URL: + // OPEN URL + shell.openExternal(action.data?.url || '') + break + case IRemoteNoticeActionType.COMMON: + // DO COMMON CASE + if (action.data?.copyToClipboard) { + clipboard.writeText(action.data.copyToClipboard) + } + if (action.data?.url) { + shell.openExternal(action.data.url) + } + break + case IRemoteNoticeActionType.SHOW_MESSAGE_BOX: { + const currentWindow = windowManager.getAvailableWindow() + dialog.showMessageBox(currentWindow, { + title: action.data?.title || '', + message: action.data?.content || '', + type: 'info', + buttons: action.data?.buttons?.map(item => item.label) || ['Yes'] + }).then(res => { + const button = action.data?.buttons?.[res.response] + if (button?.type === 'cancel') { + // do nothing + } else { + if (button?.action) { + this.doActions([button?.action]) + } + } + }) + break + } + } + } + } + } + + triggerHook (hook: IRemoteNoticeTriggerHook) { + if (!this.remoteNotice || !this.remoteNotice.list) { + return + } + const actions = this.remoteNotice.list + .filter(item => { + if (item.versionMatch) { + switch (item.versionMatch) { + case 'exact': + return item.versions.includes(app.getVersion()) + case 'gte': + return item.versions.some(version => { + // appVersion >= version + return gte(app.getVersion(), version) + }) + case 'lte': + return item.versions.some(version => { + // appVersion <= version + return lte(app.getVersion(), version) + }) + } + } + return item.versions.includes(app.getVersion()) + }) + .map(item => item.actions) + .reduce((pre, cur) => pre.concat(cur), []) + .filter(item => item.hooks.includes(hook)) + this.doActions(actions) + } +} + +const remoteNoticeHandler = new RemoteNoticeHandler() + +export { + remoteNoticeHandler +} diff --git a/src/main/apis/app/window/windowList.ts b/src/main/apis/app/window/windowList.ts index e36d2c5..44d9a9b 100644 --- a/src/main/apis/app/window/windowList.ts +++ b/src/main/apis/app/window/windowList.ts @@ -4,12 +4,13 @@ import { MINI_WINDOW_URL, RENAME_WINDOW_URL } from './constants' -import { IWindowList } from '#/types/enum' +import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum' import bus from '@core/bus' import { CREATE_APP_MENU } from '@core/bus/constants' import db from '~/main/apis/core/datastore' import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants' import { app } from 'electron' +import { remoteNoticeHandler } from '../remoteNotice' // import { i18n } from '~/main/i18n' // import { URLSearchParams } from 'url' @@ -88,6 +89,9 @@ windowList.set(IWindowList.SETTING_WINDOW, { return options }, callback (window, windowManager) { + window.once('show', () => { + remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.SETTING_WINDOW_OPEN) + }) window.loadURL(handleWindowParams(SETTING_WINDOW_URL)) window.on('closed', () => { bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false) diff --git a/src/main/lifeCycle/index.ts b/src/main/lifeCycle/index.ts index eff03e8..fd890e5 100644 --- a/src/main/lifeCycle/index.ts +++ b/src/main/lifeCycle/index.ts @@ -12,7 +12,7 @@ import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' import beforeOpen from '~/main/utils/beforeOpen' import ipcList from '~/main/events/ipcList' import busEventList from '~/main/events/busEventList' -import { IWindowList } from '#/types/enum' +import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum' import windowManager from 'apis/app/window/windowManager' import { updateShortKeyFromVersion212, @@ -35,6 +35,7 @@ import logger from 'apis/core/picgo/logger' import picgo from 'apis/core/picgo' import fixPath from './fixPath' import { initI18n } from '~/main/utils/handleI18n' +import { remoteNoticeHandler } from 'apis/app/remoteNotice' const isDevelopment = process.env.NODE_ENV !== 'production' @@ -101,6 +102,8 @@ class LifeCycle { notice.show() } } + await remoteNoticeHandler.init() + remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START) } if (!app.isReady()) { app.on('ready', readyFunction) diff --git a/src/main/utils/common.ts b/src/main/utils/common.ts index 04dd6a5..1ab0719 100644 --- a/src/main/utils/common.ts +++ b/src/main/utils/common.ts @@ -15,7 +15,9 @@ export const handleCopyUrl = (str: string): void => { export const showNotification = (options: IPrivateShowNotificationOption = { title: '', body: '', - clickToCopy: false + clickToCopy: false, + copyContent: '', + clickFn: () => {} }) => { const notification = new Notification({ title: options.title, @@ -24,7 +26,10 @@ export const showNotification = (options: IPrivateShowNotificationOption = { }) const handleClick = () => { if (options.clickToCopy) { - clipboard.writeText(options.body) + clipboard.writeText(options.copyContent || options.body) + } + if (options.clickFn) { + options.clickFn() } } notification.once('click', handleClick) diff --git a/src/universal/types/enum.ts b/src/universal/types/enum.ts index dc897e7..6361dda 100644 --- a/src/universal/types/enum.ts +++ b/src/universal/types/enum.ts @@ -27,3 +27,22 @@ export enum IWindowList { MINI_WINDOW = 'MINI_WINDOW', RENAME_WINDOW = 'RENAME_WINDOW' } + +export enum IRemoteNoticeActionType { + OPEN_URL = 'OPEN_URL', + SHOW_NOTICE = 'SHOW_NOTICE', // notification + SHOW_DIALOG = 'SHOW_DIALOG', // dialog notice + COMMON = 'COMMON', + VOID = 'VOID', // do nothing + SHOW_MESSAGE_BOX = 'SHOW_MESSAGE_BOX' +} + +export enum IRemoteNoticeTriggerHook { + APP_START = 'APP_START', + SETTING_WINDOW_OPEN = 'SETTING_WINDOW_OPEN', +} + +export enum IRemoteNoticeTriggerCount { + ONCE = 'ONCE', // default + ALWAYS = 'ALWAYS' +} diff --git a/src/universal/types/types.d.ts b/src/universal/types/types.d.ts index be72088..7cb12b0 100644 --- a/src/universal/types/types.d.ts +++ b/src/universal/types/types.d.ts @@ -219,6 +219,8 @@ interface IPrivateShowNotificationOption extends IShowNotificationOption{ * click notification to copy the body */ clickToCopy?: boolean + copyContent?: string // something to copy + clickFn?: () => void } interface IShowMessageBoxOption { @@ -348,3 +350,42 @@ interface II18nItem { label: string value: string } + +interface IRemoteNotice { + version: number + list: Array<{ + versions: string[] // matched picgo version + actions: IRemoteNoticeAction[] + versionMatch?: 'exact' | 'gte' | 'lte' + }> +} + +interface IRemoteNoticeAction { + type: import('#/types/enum').IRemoteNoticeActionType + // trigger time + hooks: import('#/types/enum').IRemoteNoticeTriggerHook[] + id: string + // trigger count: always or once; default: once + triggerCount: import('#/types/enum').IRemoteNoticeTriggerCount + + data?: { + title?: string + content?: string + desc?: string // action desc + buttons?: IRemoteNoticeButton[] + url?: string + copyToClipboard?: string + options: any // for other case + } +} + +interface IRemoteNoticeButton { + label: string + labelEN?: string + type: 'confirm' | 'cancel' | 'other' + action: IRemoteNoticeAction +} + +interface IRemoteNoticeLocalCountStorage { + [id: string]: true | number +} diff --git a/src/universal/utils/common.ts b/src/universal/utils/common.ts index 357b0a3..ff1e2ff 100644 --- a/src/universal/utils/common.ts +++ b/src/universal/utils/common.ts @@ -41,3 +41,5 @@ export const simpleClone = (obj: any) => { export const enforceNumber = (num: number | string) => { return isNaN(Number(num)) ? 0 : Number(num) } + +export const isDev = process.env.NODE_ENV === 'development'