diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..6ff27f4 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,37 @@ +on: + push: + branches: + - main + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm ci + - run: npm run release + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm ci + - run: npm run release + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - run: npm ci + - run: npm run release + diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..95c758c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.12.1 \ No newline at end of file diff --git a/main.js b/main.js index 26adc92..42819e6 100644 --- a/main.js +++ b/main.js @@ -1,10 +1,15 @@ -const { app, BrowserWindow, Menu, ipcMain, nativeTheme } = require('electron'); +const { app, BrowserWindow, Menu, ipcMain, nativeTheme, dialog, shell } = require('electron'); +const { autoUpdater } = require('electron-updater'); const path = require('path'); const isMac = process.platform === 'darwin'; +const isWin = process.platform === 'win32'; + +let autoUpdate = false; let menuObj; let mainWindow; + const menuTemplate = [ ...(isMac ? [{ role: 'appMenu' }] : []), { @@ -37,7 +42,7 @@ const menuTemplate = [ id: 'window1', enabled: true, click (menuItem, window, event) { - mainWindow.webContents.send('doSlots1'); + mainWindow.webContents.send('loadSlot',1); } }, { @@ -46,7 +51,7 @@ const menuTemplate = [ id: 'window2', enabled: true, click (menuItem, window, event) { - mainWindow.webContents.send('doSlots2'); + mainWindow.webContents.send('loadSlot',2); } }, { @@ -55,7 +60,7 @@ const menuTemplate = [ id: 'window3', enabled: true, click (menuItem, window, event) { - mainWindow.webContents.send('doSlots3'); + mainWindow.webContents.send('loadSlot',3); } }, { type: 'separator' }, @@ -71,7 +76,7 @@ const menuTemplate = [ id: 'deviceSearch', enabled: true, click (menuItem, window, event) { - mainWindow.webContents.send('doSearch'); + mainWindow.webContents.send('searchAll'); } }, { type: 'separator' }, @@ -95,13 +100,36 @@ const menuTemplate = [ id: 'deviceDelete', enabled: false, click (menuItem, window, event) { - mainWindow.webContents.send('doDelete'); + mainWindow.webContents.send('deleteActive'); } } ] - } + }, + { + label: 'Help', + role: 'help', + submenu: [ + { + label: 'About', + role: 'about' + }, + { + label: 'Check for Updates', + click: ()=>{ + autoUpdater.checkForUpdates(); + } + }, + { + label: 'Enable Auto Update', + click: ()=>{ + mainWindow.webContents.send('setAutoUpdate',!autoUpdate); + } + } + ] + }, ]; + const windowMac = { width: 1500, height: 900, @@ -130,6 +158,11 @@ const windowWin = { }, } +if (isWin) +{ + app.setAppUserModelId(app.name); +} + const createWindow = () => { nativeTheme.themeSource = 'dark'; @@ -152,7 +185,6 @@ const createWindow = () => { menuObj = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(menuObj); - }; app.whenReady().then(() => { @@ -188,3 +220,139 @@ ipcMain.on('disableSearchAll', (event, arg) => { ipcMain.on('setDevicePin', (event, arg) => { menuObj.getMenuItemById('devicePin').checked = arg; }); + + +// Autoupdate logic +ipcMain.on('checkForUpdates', (event, arg)=>{ + autoUpdater.checkForUpdates(); +}) + +ipcMain.on('setAutoUpdate', (event, _autoUpdate)=>{ + autoUpdate = _autoUpdate + + // update menu item for enabling/disabling auto update + menuTemplate[menuTemplate.length-1].submenu[2].label = autoUpdate ? 'Disable Auto Update' : 'Enable Auto Update'; + menuObj = Menu.buildFromTemplate(menuTemplate); + Menu.setApplicationMenu(menuObj); +}) + +// this can be set to true to bypass the download update dialog and skip straight to install prompt +autoUpdater.autoDownload = false; + +autoUpdater.on('update-available', (updateInfo) => { + + // skip prompting to download if autoDownload is set + if(autoUpdater.autoDownload){ + return; + } + + const msg = `Version v${updateInfo.version} is available. Would you like to download it?` + const title = 'Update Available'; + + let dialogOpts = {} + + if(isMac){ + dialogOpts = { + type: 'info', + buttons: ['Download', 'Cancel'], + title: 'Update Available', + message: title, + detail: `Auto updating is not yet automatic on MacOS. Please manually download and install version ${updateInfo.version}.` + } + }else{ + dialogOpts = { + type: 'info', + buttons: ['Download', 'Cancel'], + title: 'Update Available', + message: msg + } + } + + dialog.showMessageBox(mainWindow,dialogOpts).then((returnValue) => { + // download was clicked + if (returnValue.response === 0){ + if(!isMac){ + autoUpdater.downloadUpdate(); + }else{ + // TODO: get code signing working + // temp solution to direct user to download + shell.openExternal("https://github.com/stagehacks/Cue-View/releases/") + } + + } + }) +}) + +autoUpdater.on('update-downloaded',(event)=>{ + const title = 'Update Downloaded'; + const msg = `Version v${event.version} has been downloaded. Would you like to install this update now?` + + let dialogOpts = {} + + if(isMac){ + dialogOpts = { + type: 'info', + buttons: ['Install', 'Later'], + title: 'Update Available', + message: title, + detail: msg + } + }else{ + dialogOpts = { + type: 'info', + buttons: ['Install', 'Later'], + title: 'Update Available', + message: msg + } + } + + dialog.showMessageBox(mainWindow,dialogOpts).then((returnValue) => { + if (returnValue.response === 0) autoUpdater.quitAndInstall() + }) +}) + +autoUpdater.on('update-not-available',(updateInfo)=>{ + let dialogOpts = {} + const msg = `There is no update available at this time. Latest version is v${updateInfo.version}` + if(isMac){ + dialogOpts = { + type: 'info', + buttons: ['Ok'], + title: 'No Update Available', + message: 'No Update Available', + detail: msg + } + }else{ + dialogOpts = { + type: 'info', + buttons: ['Ok'], + title: 'No Update Available', + message: msg + } + } + + dialog.showMessageBox(mainWindow,dialogOpts) +}) + +autoUpdater.on('error',(error,message)=>{ + let dialogOpts = {} + + if(isMac){ + dialogOpts = { + type: 'error', + buttons: ['Ok'], + title: 'Update Error', + message: 'Update Error', + detail: error.message + } + }else{ + dialogOpts = { + type: 'error', + buttons: ['Ok'], + title: 'Update Error', + message: error.message + } + } + + dialog.showMessageBox(mainWindow,dialogOpts) +}) diff --git a/package-lock.json b/package-lock.json index ce71aa9..bfbdf42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -403,6 +403,11 @@ "xmlbuilder": ">=11.0.1" } }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, "@types/verror": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", @@ -570,8 +575,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "2.1.2", @@ -1057,7 +1061,6 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", - "dev": true, "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -1631,6 +1634,56 @@ } } }, + "electron-updater": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-5.3.0.tgz", + "integrity": "sha512-iKEr7yQBcvnQUPnSDYGSWC9t0eF2YbZWeYYYZzYxdl+HiRejXFENjYMnYjoOm2zxyD6Cr2JTHZhp9pqxiXuCOw==", + "requires": { + "@types/semver": "^7.3.6", + "builder-util-runtime": "9.1.1", + "fs-extra": "^10.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.3.5", + "typed-emitter": "^2.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2335,8 +2388,7 @@ "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "graceful-readlink": { "version": "1.0.1", @@ -2708,7 +2760,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -2768,8 +2819,7 @@ "lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==" }, "levn": { "version": "0.4.1", @@ -2796,6 +2846,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2817,7 +2877,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -3297,6 +3356,15 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "optional": true, + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3320,8 +3388,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "6.3.0", @@ -3657,6 +3724,12 @@ "strip-bom": "^3.0.0" } }, + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "optional": true + }, "tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -3680,6 +3753,14 @@ "dev": true, "optional": true }, + "typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "requires": { + "rxjs": "*" + } + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -3818,8 +3899,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "17.6.2", diff --git a/package.json b/package.json index cbc850f..9c44716 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cue-view", "productName": "Cue View", - "version": "0.9.6-pre", + "version": "0.9.7-pre", "description": "A dashboard for everything in your show", "main": "main.js", "scripts": { @@ -11,7 +11,8 @@ "build:mac": "electron-builder -m", "build:windows": "electron-builder -w", "build:linux": "electron-builder -l", - "build:arm": "electron-builder -l --arm64" + "build:arm": "electron-builder -l --arm64", + "release": "electron-builder" }, "author": "alec@stagehacks.com", "license": "CC BY-SA 4.0", @@ -28,6 +29,7 @@ }, "dependencies": { "bonjour": "^3.5.0", + "electron-updater": "^5.3.0", "lodash": "^4.17.20", "md5": "^2.3.0", "netmask": "^2.0.2", @@ -35,25 +37,36 @@ "uuid": "^9.0.0" }, "build": { - "appId": "com.electron.cueview", + "appId": "com.stagehacks.cueview", "icon": "./src/img/", - "artifactName": "${productName} ${os} v${version}.${ext}", + "artifactName": "${name}.${os}.v${version}.${ext}", "mac": { - "target": "zip", "category": "Utilities", "icon": "./src/img/icon.icns", "electronLanguages": [ "en" + ], + "publish": [ + "github" ] }, "win": { - "target": "portable", - "icon": "./src/img/icon.ico" + "target": "NSIS", + "icon": "./src/img/icon.ico", + "publish": [ + "github" + ] + }, + "nsis": { + "oneClick": false }, "linux": { - "target": "deb", + "target": "AppImage", "maintainer": "alec@stagehacks.com", - "category": "Utility" + "category": "Utility", + "publish": [ + "github" + ] } } } diff --git a/preload.js b/preload.js index 783827c..4272565 100644 --- a/preload.js +++ b/preload.js @@ -15,6 +15,16 @@ window.init = function init() { ipcRenderer.send('enableDeviceDropdown'); ipcRenderer.send('enableSearchAll'); + // load autoUpdate setting from storage and send to main process + const autoUpdate = JSON.parse(localStorage.getItem('autoUpdate')) + if(autoUpdate !== undefined && autoUpdate !== null){ + if(autoUpdate){ + ipcRenderer.send('checkForUpdates'); + } + // send message so main process knows the state of autoUpdate + ipcRenderer.send('setAutoUpdate', autoUpdate) + } + PLUGINS.init(() => { VIEW.init(); SAVESLOTS.loadDevices(); @@ -58,6 +68,7 @@ window.init = function init() { } }; + document.getElementById('save-slot-1').onclick = function slot1click(e) { e.stopPropagation(); SAVESLOTS.loadSlot(1); @@ -145,11 +156,11 @@ ipcRenderer.on('setActiveDevicePinned', (event, message) => { } }); -ipcRenderer.on('doSearch', (event, message) => { +ipcRenderer.on('searchAll', (event, message) => { window.searchAll(); }); -ipcRenderer.on('doDelete', (event, message) => { +ipcRenderer.on('deleteActive', (event, message) => { DEVICE.deleteActive(); }); @@ -157,17 +168,23 @@ ipcRenderer.on('resetViews', (event, message) => { SAVESLOTS.resetSlots(); }); -ipcRenderer.on('doSlots1', (event, message) => { - SAVESLOTS.loadSlot(1); +ipcRenderer.on('loadSlot', (event, slot) => { + if(slot){ + SAVESLOTS.loadSlot(slot); + } }); -ipcRenderer.on('doSlots2', (event, message) => { - SAVESLOTS.loadSlot(2); -}); -ipcRenderer.on('doSlots3', (event, message) => { - SAVESLOTS.loadSlot(3); -}); +// message from main process to set autoUpdate state +ipcRenderer.on('setAutoUpdate',(event,autoUpdate)=>{ + localStorage.setItem('autoUpdate',autoUpdate); + if(autoUpdate){ + ipcRenderer.send('checkForUpdates'); + } + // message to main process that we have updated the state + ipcRenderer.send('setAutoUpdate',autoUpdate); +}) + function switchClass(element, className) { try {