diff --git a/Types.d.ts b/Types.d.ts index 8e5c06d..7ba7765 100644 --- a/Types.d.ts +++ b/Types.d.ts @@ -32,10 +32,12 @@ interface PopulatedSchedule { sat: Series[] } -interface Settings { +interface MyStore { theme: Theme cwd: string | null lastPosterPath: string + lastUpdateCheck: number + neverCheckUpdate: boolean } interface Metadata { @@ -115,8 +117,8 @@ interface AdvFilter { interface Window { myAPI: { - changeTheme: (theme: Theme) => Promise - getSettings: () => Promise + changeTheme: (theme: Theme) => Promise + getSettings: () => Promise getSeries: () => Promise editSeries: (series: Series) => Promise changePoster: (series: Series) => Promise @@ -124,6 +126,6 @@ interface Window { getSchedule: () => Schedule changeSchedule: (schedule: Schedule) => Promise - onUpdateSettings: (listener: (newSettings: Settings) => void) => void + onUpdateSettings: (listener: (newSettings: MyStore) => void) => void } } diff --git a/package.json b/package.json index 586b4a5..1e53cd2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "my-personal-list", "description": "My personal anime tracking list", - "version": "1.3.0", + "version": "1.4.0", "scripts": { "start": "concurrently \"yarn start:renderer\" \"yarn start:main\" --kill-others", "start:main": "tsc && electron .", diff --git a/packages/common/constants.ts b/packages/common/constants.ts index 03ceaf3..39b0ca7 100644 --- a/packages/common/constants.ts +++ b/packages/common/constants.ts @@ -12,6 +12,7 @@ export enum IPCKey { RemoveUnusedPosters = 'RemoveUnusedPosters', OpenDataDir = 'OpenDataDir', ChangeDataDir = 'ChangeDataDir', + CheckForUpdate = 'CheckForUpdate', } export const DATA_FILE = 'mpl.json' diff --git a/packages/common/preload.ts b/packages/common/preload.ts index 2cdfdc6..ea62620 100644 --- a/packages/common/preload.ts +++ b/packages/common/preload.ts @@ -13,7 +13,7 @@ contextBridge.exposeInMainWorld('myAPI', { ipc.invoke(IPCKey.ChangeSchedule, schedule), // Settings subscription - onUpdateSettings: (listener: (newSettings: Settings) => void) => { + onUpdateSettings: (listener: (newSettings: MyStore) => void) => { ipc.on(IPCKey.ChangeTheme, async (e, theme) => { const settings = await ipc.invoke(IPCKey.ChangeTheme, theme) listener(settings) @@ -29,3 +29,4 @@ contextBridge.exposeInMainWorld('myAPI', { // Menu-Main communications ipc.on(IPCKey.RemoveUnusedPosters, () => ipc.invoke(IPCKey.RemoveUnusedPosters)) ipc.on(IPCKey.OpenDataDir, () => ipc.invoke(IPCKey.OpenDataDir)) +ipc.on(IPCKey.CheckForUpdate, () => ipc.invoke(IPCKey.CheckForUpdate)) diff --git a/packages/main/index.ts b/packages/main/index.ts index 37b6f4d..12cb27d 100644 --- a/packages/main/index.ts +++ b/packages/main/index.ts @@ -1,21 +1,26 @@ import { app } from 'electron' -import { autoUpdater } from 'electron-updater' +import Store from 'electron-store' import { initializeIpcEvents } from './ipcEvents' import { createMainMenu } from './menu' +import { initializeUpdater } from './updater' import { createMainWindow, win } from './windowManager' -const initializeAutoUpdate = () => { - autoUpdater.autoDownload = true - autoUpdater.autoInstallOnAppQuit = true - autoUpdater.checkForUpdatesAndNotify() -} +const store = new Store({ + defaults: { + cwd: null, + theme: 'light', + lastPosterPath: app ? app.getPath('home') : '/', + lastUpdateCheck: 0, + neverCheckUpdate: false, + }, +}) app.on('ready', async () => { createMainWindow() - createMainMenu() - initializeIpcEvents() - initializeAutoUpdate() + createMainMenu(store) + initializeIpcEvents(store) + initializeUpdater(store) // Install React Extension if in dev mode if (!app.isPackaged) { diff --git a/packages/main/ipcEvents.ts b/packages/main/ipcEvents.ts index 01f8ec0..a4db522 100644 --- a/packages/main/ipcEvents.ts +++ b/packages/main/ipcEvents.ts @@ -8,9 +8,9 @@ import { IpcMainInvokeEvent, shell, Notification, - app, } from 'electron' import { nanoid } from 'nanoid' +import { autoUpdater } from 'electron-updater' import { IPCKey, @@ -23,20 +23,20 @@ import { ensureSchedule, ensureSeries, exists, + getUpdateAvailableMsg, read, sanitizeSchedule, sanitizeSeries, - trimSeries, write, } from './util' /* Events */ export class Events { - store: Store + store: Store dialog: Dialog - constructor(store: Store, dialog: Dialog) { + constructor(store: Store, dialog: Dialog) { this.store = store this.dialog = dialog } @@ -46,7 +46,7 @@ export class Events { return this.store.store } - onGetSettings = (e: IpcMainInvokeEvent): Settings => { + onGetSettings = (e: IpcMainInvokeEvent): MyStore => { return this.store.store } @@ -162,7 +162,7 @@ export class Events { } onOpenDataDir = () => { - const cwd = store.get('cwd') + const cwd = this.store.get('cwd') if (!cwd) return new Notification({ title: 'Error Opening Data Directory', @@ -172,12 +172,12 @@ export class Events { shell.openPath(cwd) } - onChangeDataDir = async (): Promise => { + onChangeDataDir = async (): Promise => { const res = await dialog.showOpenDialog({ properties: ['openDirectory'], }) - if (res.canceled) return store.store + if (res.canceled) return this.store.store // Make sure the selected has `anime` directory in it const dirExists = await exists(path.join(res.filePaths[0], 'anime')) if (!dirExists) { @@ -186,11 +186,11 @@ export class Events { body: "Data directory must contain 'anime' dir", urgency: 'critical', }).show() - return store.store + return this.store.store } - store.set('cwd', res.filePaths[0]) - return store.store + this.store.set('cwd', res.filePaths[0]) + return this.store.store } onGetSchedule = async (e: IpcMainInvokeEvent): Promise => { @@ -220,20 +220,24 @@ export class Events { return sanitized } + + onCheckForUpdate = async (e: IpcMainInvokeEvent) => { + const res = await autoUpdater.checkForUpdates() + if (!res) return alert('No new update') + + const update = confirm(getUpdateAvailableMsg(res.updateInfo)) + if (!update) return + + await autoUpdater.downloadUpdate() + autoUpdater.quitAndInstall() + } } /* End of Events */ let initialized = false -export const store = new Store({ - defaults: { - cwd: null, - theme: 'light', - lastPosterPath: app ? app.getPath('home') : '/', - }, -}) - -export const initializeIpcEvents = () => { + +export const initializeIpcEvents = (store: Store) => { const events = new Events(store, dialog) ipcMain.handle(IPCKey.ChangeTheme, events.onChangeTheme) @@ -247,6 +251,7 @@ export const initializeIpcEvents = () => { ipcMain.handle(IPCKey.ChangeDataDir, events.onChangeDataDir) ipcMain.handle(IPCKey.GetSchedule, events.onGetSchedule) ipcMain.handle(IPCKey.ChangeSchedule, events.onChangeSchedule) + ipcMain.handle(IPCKey.CheckForUpdate, events.onCheckForUpdate) initialized = true } @@ -264,6 +269,7 @@ export const releaseIpcEvents = () => { ipcMain.removeAllListeners(IPCKey.ChangeDataDir) ipcMain.removeAllListeners(IPCKey.GetSchedule) ipcMain.removeAllListeners(IPCKey.ChangeSchedule) + ipcMain.removeAllListeners(IPCKey.CheckForUpdate) initialized = false } diff --git a/packages/main/menu.ts b/packages/main/menu.ts index 0d7a151..9a38999 100644 --- a/packages/main/menu.ts +++ b/packages/main/menu.ts @@ -1,12 +1,22 @@ import { Menu, MenuItemConstructorOptions } from 'electron' +import Store from 'electron-store' + import { IPCKey } from '../common/constants' -import { store } from './ipcEvents' -const createTemplate = (): MenuItemConstructorOptions[] => [ +const createTemplate = ( + store: Store, +): MenuItemConstructorOptions[] => [ { label: 'MyPersonalList', submenu: [ { label: 'About My Personal List' }, + { + label: 'Check For Updates', + click: async (menuItem, browser) => { + if (!browser) return + browser.webContents.send(IPCKey.CheckForUpdate) + }, + }, { type: 'separator' }, { label: 'Theme', @@ -101,7 +111,7 @@ const createTemplate = (): MenuItemConstructorOptions[] => [ }, ] -export const createMainMenu = () => { - const template = Menu.buildFromTemplate(createTemplate()) +export const createMainMenu = (store: Store) => { + const template = Menu.buildFromTemplate(createTemplate(store)) Menu.setApplicationMenu(template) } diff --git a/packages/main/updater.ts b/packages/main/updater.ts new file mode 100644 index 0000000..370b37b --- /dev/null +++ b/packages/main/updater.ts @@ -0,0 +1,33 @@ +import Store from 'electron-store' +import { autoUpdater } from 'electron-updater' +import { getUpdateAvailableMsg } from './util' + +export const initializeUpdater = async (store: Store) => { + const lastCheck = store.get('lastUpdateCheck') + const neverCheckUpdate = store.get('neverCheckUpdate') + const checked = lastCheck <= new Date().getTime() + 24 * 60 * 60 * 1000 + if (neverCheckUpdate || checked) return + + const res = await autoUpdater.checkForUpdates() + store.set('lastUpdateCheck', new Date().getTime()) + if (!res) return + + const answer = prompt( + getUpdateAvailableMsg(res.updateInfo), + 'Answer with "now" for update now, "never" for never, or ignore it for ask again tomorrow.', + ) + + switch (answer) { + case 'now': + await autoUpdater.downloadUpdate() + autoUpdater.quitAndInstall() + break + + case 'never': + store.set('neverCheckUpdate', true) + break + + default: + break + } +} diff --git a/packages/main/util.ts b/packages/main/util.ts index c9f47b6..be09e7f 100644 --- a/packages/main/util.ts +++ b/packages/main/util.ts @@ -2,6 +2,7 @@ import path from 'path' import fs from 'fs/promises' import { constants } from 'fs' import prettier from 'prettier' +import { UpdateInfo } from 'electron-updater' Object.typedKeys = Object.keys as any @@ -136,3 +137,10 @@ export const sanitizeSchedule = (schedule: Schedule): Schedule => { return newSchedule } + +export const getUpdateAvailableMsg = ( + updateInfo: UpdateInfo, + additional?: string, +) => + `Update available: ${updateInfo.version}. Update now? It will download in the background. When it's finished downloading, it will forcefully (sorry) close the app and install. ` + + additional