Skip to content

Commit 9317871

Browse files
committed
✨ Feature: add remoteNotice
1 parent a355bc0 commit 9317871

File tree

7 files changed

+277
-4
lines changed

7 files changed

+277
-4
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// get notice from remote
2+
// such as some notices for users; some updates for users
3+
import fs from 'fs-extra'
4+
import { app, clipboard, dialog, shell } from 'electron'
5+
import { IRemoteNoticeActionType, IRemoteNoticeTriggerCount, IRemoteNoticeTriggerHook } from '#/types/enum'
6+
import { lte, gte } from 'semver'
7+
import path from 'path'
8+
9+
import axios from 'axios'
10+
import windowManager from '../window/windowManager'
11+
import { showNotification } from '~/main/utils/common'
12+
import { isDev } from '~/universal/utils/common'
13+
14+
// for test
15+
const REMOTE_NOTICE_URL = isDev ? 'http://localhost:8181/remote-notice.json' : 'https://picgo-1251750343.cos.accelerate.myqcloud.com/remote-notice.yml'
16+
17+
const REMOTE_NOTICE_LOCAL_STORAGE_FILE = 'picgo-remote-notice.json'
18+
19+
const STORE_PATH = app.getPath('userData')
20+
21+
const REMOTE_NOTICE_LOCAL_STORAGE_PATH = path.join(STORE_PATH, REMOTE_NOTICE_LOCAL_STORAGE_FILE)
22+
23+
class RemoteNoticeHandler {
24+
private remoteNotice: IRemoteNotice | null = null
25+
private remoteNoticeLocalCountStorage: IRemoteNoticeLocalCountStorage | null = null
26+
27+
async init () {
28+
this.remoteNotice = await this.getRemoteNoticeInfo()
29+
this.initLocalCountStorage()
30+
}
31+
32+
private initLocalCountStorage () {
33+
const localCountStorage = {}
34+
if (!fs.existsSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH)) {
35+
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify({}))
36+
}
37+
try {
38+
const localCountStorage: IRemoteNoticeLocalCountStorage = fs.readJSONSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, 'utf8')
39+
this.remoteNoticeLocalCountStorage = localCountStorage
40+
} catch (e) {
41+
console.log(e)
42+
this.remoteNoticeLocalCountStorage = localCountStorage
43+
}
44+
}
45+
46+
private saveLocalCountStorage (newData?: IRemoteNoticeLocalCountStorage) {
47+
if (newData) {
48+
this.remoteNoticeLocalCountStorage = newData
49+
}
50+
fs.writeFileSync(REMOTE_NOTICE_LOCAL_STORAGE_PATH, JSON.stringify(this.remoteNoticeLocalCountStorage))
51+
}
52+
53+
private async getRemoteNoticeInfo (): Promise<IRemoteNotice | null> {
54+
try {
55+
const noticeInfo = await axios({
56+
method: 'get',
57+
url: REMOTE_NOTICE_URL,
58+
responseType: 'json'
59+
}).then(res => res.data) as IRemoteNotice
60+
return noticeInfo
61+
} catch {
62+
return null
63+
}
64+
}
65+
66+
/**
67+
* if the notice is not shown or is always shown, then show the notice
68+
* @param action
69+
*/
70+
private checkActionCount (action: IRemoteNoticeAction) {
71+
try {
72+
if (!this.remoteNoticeLocalCountStorage) {
73+
return true
74+
}
75+
const actionCount = this.remoteNoticeLocalCountStorage[action.id]
76+
if (actionCount === undefined) {
77+
if (action.triggerCount === IRemoteNoticeTriggerCount.ALWAYS) {
78+
this.remoteNoticeLocalCountStorage[action.id] = 1 // if always, count number
79+
} else {
80+
this.remoteNoticeLocalCountStorage[action.id] = true
81+
}
82+
return true
83+
} else {
84+
// here is the count of action
85+
// if not always show, then can't show
86+
if (action.triggerCount !== IRemoteNoticeTriggerCount.ALWAYS) {
87+
return false
88+
} else {
89+
const preCount = this.remoteNoticeLocalCountStorage[action.id]
90+
if (typeof preCount !== 'number') {
91+
this.remoteNoticeLocalCountStorage[action.id] = true
92+
return true
93+
} else {
94+
this.remoteNoticeLocalCountStorage[action.id] = preCount + 1
95+
}
96+
return true
97+
}
98+
}
99+
} finally {
100+
this.saveLocalCountStorage()
101+
}
102+
}
103+
104+
private async doActions (actions: IRemoteNoticeAction[]) {
105+
for (const action of actions) {
106+
if (this.checkActionCount(action)) {
107+
switch (action.type) {
108+
case IRemoteNoticeActionType.SHOW_DIALOG: {
109+
// SHOW DIALOG
110+
const currentWindow = windowManager.getAvailableWindow()
111+
dialog.showOpenDialog(currentWindow, action.data?.options)
112+
break
113+
}
114+
case IRemoteNoticeActionType.SHOW_NOTICE:
115+
showNotification({
116+
title: action.data?.title || '',
117+
body: action.data?.content || '',
118+
clickToCopy: !!action.data?.copyToClipboard,
119+
copyContent: action.data?.copyToClipboard || '',
120+
clickFn () {
121+
if (action.data?.url) {
122+
shell.openExternal(action.data.url)
123+
}
124+
}
125+
})
126+
break
127+
case IRemoteNoticeActionType.OPEN_URL:
128+
// OPEN URL
129+
shell.openExternal(action.data?.url || '')
130+
break
131+
case IRemoteNoticeActionType.COMMON:
132+
// DO COMMON CASE
133+
if (action.data?.copyToClipboard) {
134+
clipboard.writeText(action.data.copyToClipboard)
135+
}
136+
if (action.data?.url) {
137+
shell.openExternal(action.data.url)
138+
}
139+
break
140+
case IRemoteNoticeActionType.SHOW_MESSAGE_BOX: {
141+
const currentWindow = windowManager.getAvailableWindow()
142+
dialog.showMessageBox(currentWindow, {
143+
title: action.data?.title || '',
144+
message: action.data?.content || '',
145+
type: 'info',
146+
buttons: action.data?.buttons?.map(item => item.label) || ['Yes']
147+
}).then(res => {
148+
const button = action.data?.buttons?.[res.response]
149+
if (button?.type === 'cancel') {
150+
// do nothing
151+
} else {
152+
if (button?.action) {
153+
this.doActions([button?.action])
154+
}
155+
}
156+
})
157+
break
158+
}
159+
}
160+
}
161+
}
162+
}
163+
164+
triggerHook (hook: IRemoteNoticeTriggerHook) {
165+
if (!this.remoteNotice || !this.remoteNotice.list) {
166+
return
167+
}
168+
const actions = this.remoteNotice.list
169+
.filter(item => {
170+
if (item.versionMatch) {
171+
switch (item.versionMatch) {
172+
case 'exact':
173+
return item.versions.includes(app.getVersion())
174+
case 'gte':
175+
return item.versions.some(version => {
176+
// appVersion >= version
177+
return gte(app.getVersion(), version)
178+
})
179+
case 'lte':
180+
return item.versions.some(version => {
181+
// appVersion <= version
182+
return lte(app.getVersion(), version)
183+
})
184+
}
185+
}
186+
return item.versions.includes(app.getVersion())
187+
})
188+
.map(item => item.actions)
189+
.reduce((pre, cur) => pre.concat(cur), [])
190+
.filter(item => item.hooks.includes(hook))
191+
this.doActions(actions)
192+
}
193+
}
194+
195+
const remoteNoticeHandler = new RemoteNoticeHandler()
196+
197+
export {
198+
remoteNoticeHandler
199+
}

src/main/apis/app/window/windowList.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {
44
MINI_WINDOW_URL,
55
RENAME_WINDOW_URL
66
} from './constants'
7-
import { IWindowList } from '#/types/enum'
7+
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
88
import bus from '@core/bus'
99
import { CREATE_APP_MENU } from '@core/bus/constants'
1010
import db from '~/main/apis/core/datastore'
1111
import { TOGGLE_SHORTKEY_MODIFIED_MODE } from '#/events/constants'
1212
import { app } from 'electron'
13+
import { remoteNoticeHandler } from '../remoteNotice'
1314
// import { i18n } from '~/main/i18n'
1415
// import { URLSearchParams } from 'url'
1516

@@ -88,6 +89,9 @@ windowList.set(IWindowList.SETTING_WINDOW, {
8889
return options
8990
},
9091
callback (window, windowManager) {
92+
window.once('show', () => {
93+
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.SETTING_WINDOW_OPEN)
94+
})
9195
window.loadURL(handleWindowParams(SETTING_WINDOW_URL))
9296
window.on('closed', () => {
9397
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)

src/main/lifeCycle/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
1212
import beforeOpen from '~/main/utils/beforeOpen'
1313
import ipcList from '~/main/events/ipcList'
1414
import busEventList from '~/main/events/busEventList'
15-
import { IWindowList } from '#/types/enum'
15+
import { IRemoteNoticeTriggerHook, IWindowList } from '#/types/enum'
1616
import windowManager from 'apis/app/window/windowManager'
1717
import {
1818
updateShortKeyFromVersion212,
@@ -35,6 +35,7 @@ import logger from 'apis/core/picgo/logger'
3535
import picgo from 'apis/core/picgo'
3636
import fixPath from './fixPath'
3737
import { initI18n } from '~/main/utils/handleI18n'
38+
import { remoteNoticeHandler } from 'apis/app/remoteNotice'
3839

3940
const isDevelopment = process.env.NODE_ENV !== 'production'
4041

@@ -101,6 +102,8 @@ class LifeCycle {
101102
notice.show()
102103
}
103104
}
105+
await remoteNoticeHandler.init()
106+
remoteNoticeHandler.triggerHook(IRemoteNoticeTriggerHook.APP_START)
104107
}
105108
if (!app.isReady()) {
106109
app.on('ready', readyFunction)

src/main/utils/common.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export const handleCopyUrl = (str: string): void => {
1515
export const showNotification = (options: IPrivateShowNotificationOption = {
1616
title: '',
1717
body: '',
18-
clickToCopy: false
18+
clickToCopy: false,
19+
copyContent: '',
20+
clickFn: () => {}
1921
}) => {
2022
const notification = new Notification({
2123
title: options.title,
@@ -24,7 +26,10 @@ export const showNotification = (options: IPrivateShowNotificationOption = {
2426
})
2527
const handleClick = () => {
2628
if (options.clickToCopy) {
27-
clipboard.writeText(options.body)
29+
clipboard.writeText(options.copyContent || options.body)
30+
}
31+
if (options.clickFn) {
32+
options.clickFn()
2833
}
2934
}
3035
notification.once('click', handleClick)

src/universal/types/enum.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,22 @@ export enum IWindowList {
2727
MINI_WINDOW = 'MINI_WINDOW',
2828
RENAME_WINDOW = 'RENAME_WINDOW'
2929
}
30+
31+
export enum IRemoteNoticeActionType {
32+
OPEN_URL = 'OPEN_URL',
33+
SHOW_NOTICE = 'SHOW_NOTICE', // notification
34+
SHOW_DIALOG = 'SHOW_DIALOG', // dialog notice
35+
COMMON = 'COMMON',
36+
VOID = 'VOID', // do nothing
37+
SHOW_MESSAGE_BOX = 'SHOW_MESSAGE_BOX'
38+
}
39+
40+
export enum IRemoteNoticeTriggerHook {
41+
APP_START = 'APP_START',
42+
SETTING_WINDOW_OPEN = 'SETTING_WINDOW_OPEN',
43+
}
44+
45+
export enum IRemoteNoticeTriggerCount {
46+
ONCE = 'ONCE', // default
47+
ALWAYS = 'ALWAYS'
48+
}

src/universal/types/types.d.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ interface IPrivateShowNotificationOption extends IShowNotificationOption{
219219
* click notification to copy the body
220220
*/
221221
clickToCopy?: boolean
222+
copyContent?: string // something to copy
223+
clickFn?: () => void
222224
}
223225

224226
interface IShowMessageBoxOption {
@@ -348,3 +350,42 @@ interface II18nItem {
348350
label: string
349351
value: string
350352
}
353+
354+
interface IRemoteNotice {
355+
version: number
356+
list: Array<{
357+
versions: string[] // matched picgo version
358+
actions: IRemoteNoticeAction[]
359+
versionMatch?: 'exact' | 'gte' | 'lte'
360+
}>
361+
}
362+
363+
interface IRemoteNoticeAction {
364+
type: import('#/types/enum').IRemoteNoticeActionType
365+
// trigger time
366+
hooks: import('#/types/enum').IRemoteNoticeTriggerHook[]
367+
id: string
368+
// trigger count: always or once; default: once
369+
triggerCount: import('#/types/enum').IRemoteNoticeTriggerCount
370+
371+
data?: {
372+
title?: string
373+
content?: string
374+
desc?: string // action desc
375+
buttons?: IRemoteNoticeButton[]
376+
url?: string
377+
copyToClipboard?: string
378+
options: any // for other case
379+
}
380+
}
381+
382+
interface IRemoteNoticeButton {
383+
label: string
384+
labelEN?: string
385+
type: 'confirm' | 'cancel' | 'other'
386+
action: IRemoteNoticeAction
387+
}
388+
389+
interface IRemoteNoticeLocalCountStorage {
390+
[id: string]: true | number
391+
}

src/universal/utils/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ export const simpleClone = (obj: any) => {
4141
export const enforceNumber = (num: number | string) => {
4242
return isNaN(Number(num)) ? 0 : Number(num)
4343
}
44+
45+
export const isDev = process.env.NODE_ENV === 'development'

0 commit comments

Comments
 (0)