diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5c166c7..0000000 --- a/.eslintrc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "plugins": ["prettier"], - "extends": [ - "semistandard" - ], - "env": { - "node": true, - "jest": true - }, - "rules": { - "space-before-function-paren": 0, - "no-control-regex": 0, - "no-console": 1, - "no-var": 2, - "prefer-const": 1, - "no-prototype-builtins": 0, - "no-useless-escape": 1, - "comma-dangle": 0, - "keyword-spacing": ["error", { "before": true, "after": true }], - "comma-spacing": ["error", { "before": false, "after": true }], - "indent": 0, - "prefer-spread": 1, - "no-mixed-operators": 1, - "prettier/prettier": 1, - "camelcase": ["error", { - "allow": ["^UNSAFE_"], - "properties": "never", - "ignoreGlobals": true - }], - "quotes": [ - "error", "single", { - "avoidEscape": true, - "allowTemplateLiterals": true - } - ], - } -} diff --git a/.prettierrc b/.prettierrc index d9c33d2..0dc3fc7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -49,7 +49,7 @@ useTabs: false # @default : true # @ref : https://prettier.io/docs/en/options.html#semicolons # -------------------------------------------------------------------------------------- -semi: true +semi: false # -------------------------------------------------------------------------------------- # Use single quotes instead of double quotes. diff --git a/index.js b/index.js index cc71c54..c789e9b 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,12 @@ -const { app, BrowserWindow, Tray, Menu, MenuItem } = require('electron'); -const electronShell = require('electron').shell; -const toasted = require('toasted-notifier'); -const process = require('process'); -const path = require('path'); -const moment = require('moment'); -const Store = require('./store.js'); +/* eslint-disable no-unused-vars */ +/* eslint-disable no-console */ +const { app, BrowserWindow, Tray, Menu, MenuItem } = require( 'electron' ) +const electronShell = require( 'electron' ).shell +const toasted = require( 'toasted-notifier' ) +const process = require( 'process' ) +const path = require( 'path' ) +const moment = require( 'moment' ) +const Storage = require( './app/Storage.js' ) /* Declare > Prompt @@ -12,32 +14,34 @@ const Store = require('./store.js'); @docs : https://araxeus.github.io/custom-electron-prompt/ */ -const prompt = require('custom-electron-prompt'); +const prompt = require( 'custom-electron-prompt' ) /* Debug > Print args */ -console.log(process.argv); +console.log( 'env' ) +console.log( process.env.ENV ) +console.log( process.argv ) /* Declare > Package */ -const packageJson = require('./package.json'); -const appVer = packageJson.version; -const appName = packageJson.name; -const appAuthor = packageJson.author; -const appElectron = process.versions.electron; -const appRepo = packageJson.repository; -const appIcon = app.getAppPath() + '/assets/icons/ntfy.png'; +const pkgJson = require( './package.json' ) +const appVer = pkgJson.version +const appName = pkgJson.name +const appAuthor = pkgJson.author +const appElectron = process.versions.electron +const appRepo = pkgJson.repository +const appIcon = app.getAppPath() + '/assets/icons/ntfy.png' /* Declare > Window */ -let winMain, winAbout, timerPollrate, tray; +let winMain, winAbout, timerPollrate, tray /* Declare > CLI State @@ -48,19 +52,19 @@ let winMain, winAbout, timerPollrate, tray; bQuitOnClose --quit when pressing top-right close button, app exits instead of going to tray */ -let bDevTools = 0; -let bHotkeysEnabled = 0; -let bQuitOnClose = 0; -let bStartHidden = 0; -let bWinHidden = 0; +let bDevTools = 0 +let bHotkeysEnabled = 0 +let bQuitOnClose = 0 +const bStartHidden = 0 +let bWinHidden = 0 /* Declare > Status */ -let statusHasError = false; -let statusBadURL = false; -let statusMessage; +let statusHasError = false +let statusBadURL = false +let statusMessage /* Declare > Fallback @@ -68,9 +72,9 @@ let statusMessage; fallback values in case a user does something unforseen to cause an index error */ -const _Instance = 'https://ntfy.sh/app'; -const _Datetime = 'YYYY-MM-DD hh:mm a'; -const _Pollrate = 5; +const _Instance = 'https://ntfy.sh/app' +const _Datetime = 'YYYY-MM-DD hh:mm a' +const _Pollrate = 5 /* Declare > Store Values @@ -79,7 +83,7 @@ const _Pollrate = 5; storage: AppData\Roaming\ntfy-desktop */ -const store = new Store({ +const store = new Storage( { configName: 'prefs', defaults: { instanceURL: _Instance, @@ -92,25 +96,31 @@ const store = new Store({ bPersistentNoti: 0, datetime: _Datetime } -}); +} ) /* Validate instance url */ -function validateUrl(uri, tries, delay) { - return new Promise((success, reject) => { - (function rec(i) { - fetch(uri, {mode: 'no-cors'}).then((r) => { - success(r); // success: resolve promise - }).catch( err => { - if (tries === 0) // num of tries reached - return reject(err); - - setTimeout(() => rec(--tries), delay ) // retry - }); // retries exceeded - })(tries); - }); +function validateUrl( uri, tries, delay ) +{ + return new Promise( ( success, reject ) => + { + ( function rec( _i ) + { + fetch( uri, { mode: 'no-cors' } ).then( ( r ) => + { + success( r ) // success: resolve promise + } + ).catch( ( err ) => + { + if ( tries === 0 ) // num of tries reached + return reject( err ) + + setTimeout( () => rec( --tries ), delay ) // retry + } ) // retries exceeded + } )( tries ) + } ) } /* @@ -122,15 +132,17 @@ function validateUrl(uri, tries, delay) { API Token can be specified in app. */ -async function GetMessageData(uri) { - const cfgApiToken = store.get('apiToken'); - let req = await fetch(uri, { - method: 'GET', - headers: { - 'Accept': 'application/json', - Authorization: `Bearer ${cfgApiToken}` - } - }); +async function GetMessageData( uri ) +{ + const cfgApiToken = store.get( 'apiToken' ) + const req = await fetch( uri, + { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${ cfgApiToken }` + } + } ) /* ntfy has the option to output message results as json, however the structure of that json @@ -140,22 +152,24 @@ async function GetMessageData(uri) { array. */ - const json = await req.text(); - let jsonArr = []; - const entries = json.split('\n'); - for (let i = 0; i < entries.length; i++) { - jsonArr.push(entries[i]); + const json = await req.text() + const jsonArr = [] + const entries = json.split( '\n' ) + for ( let i = 0; i < entries.length; i++ ) + { + jsonArr.push( entries[ i ] ) } /* Filter out empty entry in array which was caused by the last newline */ - const jsonResult = jsonArr.filter(function (el) { - return el != null && el != ''; - }); + const jsonResult = jsonArr.filter( function ( el ) + { + return el != null && el != '' + } ) - return jsonResult; + return jsonResult } /* @@ -165,114 +179,120 @@ async function GetMessageData(uri) { @ref : https://docs.ntfy.sh/subscribe/api/#poll-for-messages */ -const msgHistory = []; -async function GetMessages() { +const msgHistory = [] +async function GetMessages() +{ - const cfgPollrate = store.get('pollrate') || _Pollrate; - const cfgTopics = store.get('topics'); - const cfgInstanceURL = store.get('instanceURL'); + const cfgPollrate = store.get( 'pollrate' ) || _Pollrate + const cfgTopics = store.get( 'topics' ) + const cfgInstanceURL = store.get( 'instanceURL' ) - if (cfgInstanceURL === '' || cfgInstanceURL === null) { - console.log(`URL Missing, skipping GetMessages(): ${uri}`); - return; + if ( cfgInstanceURL === '' || cfgInstanceURL === null ) + { + console.log( `URL Missing, skipping GetMessages(): ${ uri }` ) + return } - let uri = `${cfgInstanceURL}/${cfgTopics}/json?since=${cfgPollrate}s&poll=1`; - console.log(`URL: ${uri}`); + let uri = `${ cfgInstanceURL }/${ cfgTopics }/json?since=${ cfgPollrate }s&poll=1` + console.log( `URL: ${ uri }` ) /* For the official ntfy.sh API, url must be changed internally https://ntfy.sh/app/ -> https://ntfy.sh/ */ - if (uri.includes('ntfy.sh/app')) { - uri = uri.replace("ntfy.sh/app", 'ntfy.sh'); + if ( uri.includes( 'ntfy.sh/app' ) ) + { + uri = uri.replace( 'ntfy.sh/app', 'ntfy.sh' ) } /* Bad URL detected, skip polling */ - if ( statusBadURL == true ) { - console.error(`Invalid instance URL specified, skipping polling`); - return; + if ( statusBadURL == true ) + { + console.error( `Invalid instance URL specified, skipping polling` ) + return } - const json = await GetMessageData(uri); + const json = await GetMessageData( uri ) - console.log(`CHECKING FOR NEW MESSAGES`); - console.log(`---------------------------------------------------------`); - console.log(`InstanceURL ........... ${cfgInstanceURL}`); - console.log(`Query ................. ${uri}`); - console.log(`Topics ................ ${cfgTopics}`); + console.log( `CHECKING FOR NEW MESSAGES` ) + console.log( `---------------------------------------------------------` ) + console.log( `InstanceURL ........... ${ cfgInstanceURL }` ) + console.log( `Query ................. ${ uri }` ) + console.log( `Topics ................ ${ cfgTopics }` ) /* Loop ntfy api results. only items with event = 'message' will be allowed through to display in a notification. */ - console.log(`---------------------------------------------------------`); - console.log(`History ............... ${msgHistory}`); - console.log(`Messages .............. ${JSON.stringify(json)}`); - console.log(`---------------------------------------------------------\n`); + console.log( `---------------------------------------------------------` ) + console.log( `History ............... ${ msgHistory }` ) + console.log( `Messages .............. ${ JSON.stringify( json ) }` ) + console.log( `---------------------------------------------------------\n` ) - for (let i = 0; i < json.length; i++) { - const object = JSON.parse(json[i]); - const id = object.id; - const type = object.event; - const time = object.time; - const expires = object.expires; - const message = object.message; - const topic = object.topic; + for ( let i = 0; i < json.length; i++ ) + { + const object = JSON.parse( json[ i ] ) + const id = object.id + const type = object.event + const time = object.time + const expires = object.expires + const message = object.message + const topic = object.topic - const cfgPersistent = store.get('bPersistentNoti') == 0 ? false : true; - const cfgInstanceURL = store.get('instanceURL'); + const cfgPersistent = store.get( 'bPersistentNoti' ) == 0 ? false : true + const cfgInstanceURL = store.get( 'instanceURL' ) - if (type != 'message') - continue; + if ( type != 'message' ) + continue /* convert unix timestamp into human readable */ - const dateHuman = moment.unix(time).format(store.get('datetime' || _Datetime)); + const dateHuman = moment.unix( time ).format( store.get( 'datetime' || _Datetime ) ) /* debugging to console to show the status of messages */ - const msgStatus = msgHistory.includes(id) === true ? 'already sent, skipping' : 'pending send'; - console.log(`Messages .............. ${type}:${id} ${msgStatus}`); + const msgStatus = msgHistory.includes( id ) === true ? 'already sent, skipping' : 'pending send' + console.log( `Messages .............. ${ type }:${ id } ${ msgStatus }` ) /* @ref : https://github.com/Aetherinox/toasted-notifier */ - if (!msgHistory.includes(id)) { - toasted.notify({ - title: `${topic} - ${dateHuman}`, - subtitle: `${dateHuman}`, - message: `${message}`, + if ( !msgHistory.includes( id ) ) + { + toasted.notify( { + title: `${ topic } - ${ dateHuman }`, + subtitle: `${ dateHuman }`, + message: `${ message }`, sound: 'Pop', open: cfgInstanceURL, persistent: cfgPersistent, sticky: cfgPersistent - }); + } ) - msgHistory.push(id); + msgHistory.push( id ) - console.log(` Topic .............. ${type}:${id} ${topic}`); - console.log(` Date ............... ${type}:${id} ${dateHuman}`); - console.log(` InstanceURL ........ ${type}:${id} ${cfgInstanceURL}`); - console.log(` Persistent ......... ${type}:${id} ${cfgPersistent}`); + console.log( ` Topic .............. ${ type }:${ id } ${ topic }` ) + console.log( ` Date ............... ${ type }:${ id } ${ dateHuman }` ) + console.log( ` InstanceURL ........ ${ type }:${ id } ${ cfgInstanceURL }` ) + console.log( ` Persistent ......... ${ type }:${ id } ${ cfgPersistent }` ) } - console.log(`Messages .............. ${type}:${id} sent`); + console.log( `Messages .............. ${ type }:${ id } sent` ) } - console.log(`\n\n`); + console.log( `\n\n` ) - return json; + return json } /* @@ -284,255 +304,276 @@ async function GetMessages() { */ const menu_Main = [ -{ - label: '&App', - id: 'app', - submenu: [ - { - label: 'Quit', - id: 'quit', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+Q' : '', - click: function () { - app.isQuiting = true; - app.quit(); + { + label: '&App', + id: 'app', + submenu: [ + { + label: 'Quit', + id: 'quit', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+Q' : '', + click: function () + { + app.isQuiting = true + app.quit() + } } - } - ] -}, -{ - label: '&Configure', - id: 'configure', - submenu: [ - { - label: 'General', - id: 'general', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+G' : '', - click: function () { - prompt( - { - title: 'General Settings', - label: 'General Settings
Change the overall functionality of the app.
', - useHtmlLabel: true, - alwaysOnTop: true, - type: 'multiInput', - resizable: false, - customStylesheet: path.join(__dirname, `pages`, `css`, `prompt.css`), - height: 480, - icon: appIcon, - multiInputOptions: + ] + }, + { + label: '&Configure', + id: 'configure', + submenu: [ + { + label: 'General', + id: 'general', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+G' : '', + click: function () + { + prompt( + { + title: 'General Settings', + label: 'General Settings
Change the overall functionality of the app.
', + useHtmlLabel: true, + alwaysOnTop: true, + type: 'multiInput', + resizable: false, + customStylesheet: path.join( __dirname, `pages`, `css`, `prompt.css` ), + height: 480, + icon: appIcon, + multiInputOptions: [ { label: 'Developer tools in app menu', selectOptions: { 0: 'Disabled', 1: 'Enabled' }, - value: store.get('bDevTools'), + value: store.get( 'bDevTools' ) }, { label: 'Allow usage of hotkeys to navigate', selectOptions: { 0: 'Disabled', 1: 'Enabled' }, - value: store.get('bHotkeys'), + value: store.get( 'bHotkeys' ) }, { label: 'Quit app instead of send-to-tray for close button', selectOptions: { 0: 'Disabled', 1: 'Enabled' }, - value: store.get('bQuitOnClose'), + value: store.get( 'bQuitOnClose' ) }, { label: 'Start app minimized in tray', selectOptions: { 0: 'Disabled', 1: 'Enabled' }, - value: store.get('bStartHidden'), + value: store.get( 'bStartHidden' ) } - ], - }, - winMain - ) - .then((response) => { - if (response !== null) { - // do not update dev tools if value hasn't changed - if ( store.get('bDevTools') !== response[0]) + ] + }, + winMain + ) + .then( ( response ) => { - store.set('bDevTools', response[0]); - activeDevTools(); - } + if ( response !== null ) + { + // do not update dev tools if value hasn't changed + if ( store.get( 'bDevTools' ) !== response[ 0 ] ) + { + store.set( 'bDevTools', response[ 0 ] ) + activeDevTools() + } - store.set('bHotkeys', response[1]); - store.set('bQuitOnClose', response[2]); - store.set('bStartHidden', response[3]); - } - }) - .catch((response) => { - console.error - }) + store.set( 'bHotkeys', response[ 1 ] ) + store.set( 'bQuitOnClose', response[ 2 ] ) + store.set( 'bStartHidden', response[ 3 ] ) + } + } ) + .catch( ( _response ) => + { + console.error + } ) /* setTimeout(function (){ BrowserWindow.getFocusedWindow().webContents.openDevTools(); }, 3000); */ - } - }, - { - label: 'URL', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+U' : '', - click: function () { - prompt( - { - title: 'Set Server Instance', - label: 'Server URL
This can either be the URL to the official ntfy.sh server, or your own self-hosted domain / ip.

Remove everything to set back to official ntfy.sh server.
', - useHtmlLabel: true, - value: store.get('instanceURL') || _Instance, - alwaysOnTop: true, - type: 'input', - customStylesheet: path.join(__dirname, `pages`, `css`, `prompt.css`), - height: 290, - icon: appIcon, - inputAttrs: { - type: 'url' - } - }, - winMain - ) - .then((response) => { - if (response !== null) { - const newUrl = (response === "" ? _Instance : response); - store.set('instanceURL', newUrl); - - /* + } + }, + { + label: 'URL', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+U' : '', + click: function () + { + prompt( + { + title: 'Set Server Instance', + label: 'Server URL
This can either be the URL to the official ntfy.sh server, or your own self-hosted domain / ip.

Remove everything to set back to official ntfy.sh server.
', + useHtmlLabel: true, + value: store.get( 'instanceURL' ) || _Instance, + alwaysOnTop: true, + type: 'input', + customStylesheet: path.join( __dirname, `pages`, `css`, `prompt.css` ), + height: 290, + icon: appIcon, + inputAttrs: { + type: 'url' + } + }, + winMain + ) + .then( ( response ) => + { + if ( response !== null ) + { + const newUrl = ( response === '' ? _Instance : response ) + store.set( 'instanceURL', newUrl ) + + /* Validate URL. Invalid URLs should not perform polling. load default _Instance url */ - validateUrl(store.get('instanceURL'), 3, 1000).then( item => { - statusBadURL = false; - console.log(`Successfully resolved `+ store.get('instanceURL')); - winMain.loadURL(store.get('instanceURL')); - }).catch( err => { - statusBadURL = true; - const msg = `Failed to resolve `+ store.get('instanceURL') + ` - defaulting to ${_Instance}`; - statusMessage = `${msg}`; - console.error(`${msg}`); - store.set('instanceURL', _Instance); - winMain.loadURL(_Instance); - }); - } - }) - .catch((response) => { - console.error - }) + validateUrl( store.get( 'instanceURL' ), 3, 1000 ).then( ( _item ) => + { + statusBadURL = false + console.log( `Successfully resolved ` + store.get( 'instanceURL' ) ) + winMain.loadURL( store.get( 'instanceURL' ) ) + } ).catch( ( _err ) => + { + statusBadURL = true + const msg = `Failed to resolve ` + store.get( 'instanceURL' ) + ` - defaulting to ${ _Instance }` + statusMessage = `${ msg }` + console.error( `${ msg }` ) + store.set( 'instanceURL', _Instance ) + winMain.loadURL( _Instance ) + } ) + } + } ) + .catch( ( _response ) => + { + console.error + } ) /* setTimeout(function (){ BrowserWindow.getFocusedWindow().webContents.openDevTools(); }, 3000); */ - } - }, - { - label: 'API Token', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+T' : '', - click: function () { - prompt( - { - title: 'Set API Token', - label: 'API Token
Generate an API token within ntfy.sh or your self-hosted instance and provide it below to receive desktop push notifications.
', - useHtmlLabel: true, - value: store.get('apiToken'), - alwaysOnTop: true, - type: 'input', - customStylesheet: path.join(__dirname, `pages`, `css`, `prompt.css`), - height: 265, - icon: appIcon, - inputAttrs: { - type: 'text' - } - }, - winMain - ) - .then((response) => { - if (response !== null) { - store.set('apiToken', response); - } - }) - .catch((response) => { - console.error - }) - } - }, - { - label: 'Topics', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+SHIFT+T' : '', - click: function () { - prompt( - { - title: 'Set Subscribed Topics', - label: 'Subscribed Topics
Specify a list of topics you would like to receive push notifications for, separated by commas.

Ex: Meetings,Personal,Urgent
', - useHtmlLabel: true, - value: store.get('topics'), - alwaysOnTop: true, - type: 'input', - customStylesheet: path.join(__dirname, `pages`, `css`, `prompt.css`), - height: 290, - icon: appIcon, - inputAttrs: { - type: 'text' - } - }, - winMain - ) - .then((response) => { - if (response !== null) { - // do not update topics unless values differ from original, since we need to reload the page - if ( store.get('topics') !== response) + } + }, + { + label: 'API Token', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+T' : '', + click: function () + { + prompt( { - store.set('topics', response); - - if (typeof (store.get('instanceURL')) !== 'string' || store.get('instanceURL') === '' || store.get('instanceURL') === null ) { - store.set('instanceURL', _Instance); + title: 'Set API Token', + label: 'API Token
Generate an API token within ntfy.sh or your self-hosted instance and provide it below to receive desktop push notifications.
', + useHtmlLabel: true, + value: store.get( 'apiToken' ), + alwaysOnTop: true, + type: 'input', + customStylesheet: path.join( __dirname, `pages`, `css`, `prompt.css` ), + height: 265, + icon: appIcon, + inputAttrs: { + type: 'text' + } + }, + winMain + ) + .then( ( response ) => + { + if ( response !== null ) + { + store.set( 'apiToken', response ) + } + } ) + .catch( ( _response ) => + { + console.error + } ) + } + }, + { + label: 'Topics', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+SHIFT+T' : '', + click: function () + { + prompt( + { + title: 'Set Subscribed Topics', + label: 'Subscribed Topics
Specify a list of topics you would like to receive push notifications for, separated by commas.

Ex: Meetings,Personal,Urgent
', + useHtmlLabel: true, + value: store.get( 'topics' ), + alwaysOnTop: true, + type: 'input', + customStylesheet: path.join( __dirname, `pages`, `css`, `prompt.css` ), + height: 290, + icon: appIcon, + inputAttrs: { + type: 'text' } + }, + winMain + ) + .then( ( response ) => + { + if ( response !== null ) + { + // do not update topics unless values differ from original, since we need to reload the page + if ( store.get( 'topics' ) !== response ) + { + store.set( 'topics', response ) - winMain.loadURL(store.get('instanceURL')); - } - } - }) - .catch((response) => { - console.error - }) - } - }, - { - label: 'Notifications', - accelerator: (bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) ? 'CTRL+N' : '', - click: function () { - prompt( - { - title: 'Notifications', - label: 'Notification Settings
Determines how notifications will behave
', - useHtmlLabel: true, - alwaysOnTop: true, - type: 'multiInput', - resizable: false, - customStylesheet: path.join(__dirname, `pages`, `css`, `prompt.css`), - height: 400, - icon: appIcon, - multiInputOptions: + if ( typeof ( store.get( 'instanceURL' ) ) !== 'string' || store.get( 'instanceURL' ) === '' || store.get( 'instanceURL' ) === null ) + { + store.set( 'instanceURL', _Instance ) + } + + winMain.loadURL( store.get( 'instanceURL' ) ) + } + } + } ) + .catch( ( _response ) => + { + console.error + } ) + } + }, + { + label: 'Notifications', + accelerator: ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) ? 'CTRL+N' : '', + click: function () + { + prompt( + { + title: 'Notifications', + label: 'Notification Settings
Determines how notifications will behave
', + useHtmlLabel: true, + alwaysOnTop: true, + type: 'multiInput', + resizable: false, + customStylesheet: path.join( __dirname, `pages`, `css`, `prompt.css` ), + height: 400, + icon: appIcon, + multiInputOptions: [ { label: 'Stay on screen until dismissed', selectOptions: { 0: 'Disabled', 1: 'Enabled' }, - value: store.get('bPersistentNoti'), + value: store.get( 'bPersistentNoti' ) }, { label: 'Datetime format for notification title', - value: store.get('datetime') || _Datetime, + value: store.get( 'datetime' ) || _Datetime, inputAttrs: { - placeholder: `${_Datetime}`, + placeholder: `${ _Datetime }`, required: true } }, { label: 'Polling rate / fetch messages (seconds)', - value: store.get('pollrate') || _Pollrate, + value: store.get( 'pollrate' ) || _Pollrate, inputAttrs: { type: 'number', required: true, @@ -541,120 +582,129 @@ const menu_Main = [ } } ] - }, - winMain - ) - .then((response) => { - if (response !== null) { - store.set('bPersistentNoti', response[0]) - store.set('datetime', response[1]) - store.set('pollrate', response[2]) - - const cfgPollrate = (store.get('pollrate') || _Pollrate); - const fetchInterval = (cfgPollrate * 1000) + 600; - clearInterval(timerPollrate); - timerPollrate = setInterval(GetMessages, fetchInterval); - } - }) - .catch((response) => { - console.error - }) + }, + winMain + ) + .then( ( response ) => + { + if ( response !== null ) + { + store.set( 'bPersistentNoti', response[ 0 ] ) + store.set( 'datetime', response[ 1 ] ) + store.set( 'pollrate', response[ 2 ] ) + + const cfgPollrate = ( store.get( 'pollrate' ) || _Pollrate ) + const fetchInterval = ( cfgPollrate * 1000 ) + 600 + clearInterval( timerPollrate ) + timerPollrate = setInterval( GetMessages, fetchInterval ) + } + } ) + .catch( ( _response ) => + { + console.error + } ) - /* + /* setTimeout(function (){ BrowserWindow.getFocusedWindow().webContents.openDevTools(); }, 3000); */ + } } - } - ] -}, -{ - label: '&Help', - id: 'help', - submenu: [ - { - id: 'about', - label: 'About', - click() { - const aboutTitle = `About`; - winAbout = new BrowserWindow({ - width: 480, - height: 440, - title: `${aboutTitle}`, - icon: appIcon, - parent: winMain, - center: true, - resizable: false, - fullscreenable: false, - minimizable: false, - maximizable: false, - modal: true, - backgroundColor: '#212121', - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - enableRemoteModule: true - } - }); - - winAbout.loadFile(path.join(__dirname, `pages`, `about.html`)).then(() => { - winAbout.webContents - .executeJavaScript( - ` - setTitle('${aboutTitle}'); - setAppInfo('${appRepo}', '${appName}', '${appVer}', '${appAuthor}', '${appElectron}');`, - true - ) - .then((result) => {}) - .catch(console.error); - }); - - winAbout.webContents.on('new-window', function (e, url) { - e.preventDefault(); - require('electron').shell.openExternal(url); - }); + ] + }, + { + label: '&Help', + id: 'help', + submenu: [ + { + id: 'about', + label: 'About', + click() + { + const aboutTitle = `About` + winAbout = new BrowserWindow( { + width: 480, + height: 440, + title: `${ aboutTitle }`, + icon: appIcon, + parent: winMain, + center: true, + resizable: false, + fullscreenable: false, + minimizable: false, + maximizable: false, + modal: true, + backgroundColor: '#212121', + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true + } + } ) - // Remove menubar from about window - winAbout.setMenu(null); - } - }, - { - label: 'View New Releases', - click() { - electronShell.openExternal(`${packageJson.homepage}`); + winAbout.loadFile( path.join( __dirname, `pages`, `about.html` ) ).then( () => + { + winAbout.webContents + .executeJavaScript( + ` + setTitle('${ aboutTitle }'); + setAppInfo('${ appRepo }', '${ appName }', '${ appVer }', '${ appAuthor }', '${ appElectron }');`, + true + ) + .then( ( _result ) => {} ) + .catch( console.error ) + } ) + + winAbout.webContents.on( 'new-window', function ( e, url ) + { + e.preventDefault() + require( 'electron' ).shell.openExternal( url ) + } ) + + // Remove menubar from about window + winAbout.setMenu( null ) + } + }, + { + label: 'View New Releases', + click() + { + electronShell.openExternal( `${ pkgJson.homepage }` ) + } } - } - ] -}]; + ] + }] /* Tray > Context Menu */ -const contextMenu = Menu.buildFromTemplate([ +const contextMenu = Menu.buildFromTemplate( [ { label: 'Show App', - click: function () { - winMain.show(); + click: function () + { + winMain.show() } }, { label: 'Quit', - click: function () { - app.isQuiting = true; - app.quit(); + click: function () + { + app.isQuiting = true + app.quit() } } -]); +] ) /* Main Menu > Set */ -const header_menu = Menu.buildFromTemplate(menu_Main); -Menu.setApplicationMenu(header_menu); +const header_menu = Menu.buildFromTemplate( menu_Main ) +Menu.setApplicationMenu( header_menu ) /* Main Menu > Developer Tools @@ -665,24 +715,27 @@ Menu.setApplicationMenu(header_menu); App | Configure | Help */ -function activeDevTools() { - const header_menu = Menu.buildFromTemplate(menu_Main); - Menu.setApplicationMenu(header_menu); - - if (bDevTools == 1 || store.get('bDevTools') == 1) { - let menuItem = header_menu.getMenuItemById('app') +function activeDevTools() +{ + const header_menu = Menu.buildFromTemplate( menu_Main ) + Menu.setApplicationMenu( header_menu ) - menuItem.submenu.insert(0, new MenuItem( - { - label: 'Toggle Dev Tools', - accelerator: process.platform === 'darwin' ? 'ALT+CMD+I' : 'CTRL+SHIFT+I', - click: () => { - winMain.webContents.toggleDevTools(); - } - }, - { - type: 'separator' - })) + if ( bDevTools == 1 || store.get( 'bDevTools' ) == 1 ) + { + const menuItem = header_menu.getMenuItemById( 'app' ) + + menuItem.submenu.insert( 0, new MenuItem( + { + label: 'Toggle Dev Tools', + accelerator: process.platform === 'darwin' ? 'ALT+CMD+I' : 'CTRL+SHIFT+I', + click: () => + { + winMain.webContents.toggleDevTools() + } + }, + { + type: 'separator' + } ) ) } } @@ -690,19 +743,20 @@ function activeDevTools() { App > Ready */ -function ready() { +function ready() +{ /* New Window */ - winMain = new BrowserWindow({ - title: `${appName}`, + winMain = new BrowserWindow( { + title: `${ appName }`, width: 1280, height: 720, icon: appIcon, backgroundColor: '#212121' - }); + } ) /* Load default url to main window @@ -711,11 +765,12 @@ function ready() { otherwise app will return invalid index and stop loading. */ - if (typeof (store.get('instanceURL')) !== 'string' || store.get('instanceURL') === '' || store.get('instanceURL') === null ) { - store.set('instanceURL', _Instance); + if ( typeof ( store.get( 'instanceURL' ) ) !== 'string' || store.get( 'instanceURL' ) === '' || store.get( 'instanceURL' ) === null ) + { + store.set( 'instanceURL', _Instance ) - statusHasError = true; - statusMessage = `Invalid instance URL specified; defaulting to ${_Instance}`; + statusHasError = true + statusMessage = `Invalid instance URL specified; defaulting to ${ _Instance }` } /* @@ -724,26 +779,29 @@ function ready() { load default _Instance url */ - validateUrl(store.get('instanceURL'), 3, 1000).then( item => { - statusBadURL = false; - console.log(`Successfully resolved `+ store.get('instanceURL')); - winMain.loadURL(store.get('instanceURL')); - }).catch( err => { - statusBadURL = true; - const msg = `Failed to resolve `+ store.get('instanceURL') + ` - defaulting to ${_Instance}`; - statusMessage = `${msg}`; - console.error(`${msg}`); - store.set('instanceURL', _Instance); - winMain.loadURL(_Instance); - }); + validateUrl( store.get( 'instanceURL' ), 3, 1000 ).then( ( _item ) => + { + statusBadURL = false + console.log( `Successfully resolved ` + store.get( 'instanceURL' ) ) + winMain.loadURL( store.get( 'instanceURL' ) ) + } ).catch( ( _err ) => + { + statusBadURL = true + const msg = `Failed to resolve ` + store.get( 'instanceURL' ) + ` - defaulting to ${ _Instance }` + statusMessage = `${ msg }` + console.error( `${ msg }` ) + store.set( 'instanceURL', _Instance ) + winMain.loadURL( _Instance ) + } ) /* Event > Page Title Update */ - winMain.on('page-title-updated', (e) => { - e.preventDefault(); - }); + winMain.on( 'page-title-updated', ( e ) => + { + e.preventDefault() + } ) /* Event > Close @@ -752,27 +810,33 @@ function ready() { otherwise; app will hide */ - winMain.on('close', function (e) { - if (!app.isQuiting) { - e.preventDefault(); - if (bQuitOnClose == 1 || store.get('bQuitOnClose') == 1) { - app.isQuiting = true; - app.quit(); - } else { - winMain.hide(); + winMain.on( 'close', function ( e ) + { + if ( !app.isQuiting ) + { + e.preventDefault() + if ( bQuitOnClose == 1 || store.get( 'bQuitOnClose' ) == 1 ) + { + app.isQuiting = true + app.quit() + } + else + { + winMain.hide() } } - return false; - }); + return false + } ) /* Event > Closed */ - winMain.on('closed', () => { - winMain = null; - }); + winMain.on( 'closed', () => + { + winMain = null + } ) /* Event > New Window @@ -780,21 +844,24 @@ function ready() { buttons leading to external websites should open in user browser */ - winMain.webContents.on('new-window', (e, url) => { - e.preventDefault(); - require('electron').shell.openExternal(url); - }); + winMain.webContents.on( 'new-window', ( e, url ) => + { + e.preventDefault() + require( 'electron' ).shell.openExternal( url ) + } ) /* Display footer div on website if something has gone wrong. user shouldn't see this unless its something serious */ - winMain.webContents.on('did-finish-load', (e, url)=> { - if ((statusHasError === true || statusBadURL == true) && statusMessage !== '') { + winMain.webContents.on( 'did-finish-load', ( _e, _url ) => + { + if ( ( statusHasError === true || statusBadURL == true ) && statusMessage !== '' ) + { winMain.webContents .executeJavaScript( - ` + ` const div = document.createElement("div"); div.style.position = "sticky"; div.style.height = "34px"; @@ -811,79 +878,89 @@ function ready() { const span = document.createElement("span"); span.setAttribute("class","ntfy-notify error"); - span.textContent = '${statusMessage}'; + span.textContent = '${ statusMessage }'; div.appendChild(span); document.body.appendChild(div); - `) - } + ` ) } - ); + } + ) /* Event > Input */ - winMain.webContents.on('before-input-event', (e, input) => { + winMain.webContents.on( 'before-input-event', ( _e, input ) => + { /* Input > Refresh Page (CTRL + r) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === 'r') { - winMain.webContents.reload(); + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === 'r' ) + { + winMain.webContents.reload() } /* Input > Zoom In (CTRL + =) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === '=') { - winMain.webContents.zoomFactor += 0.1; + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === '=' ) + { + winMain.webContents.zoomFactor += 0.1 } /* Input > Zoom Out (CTRL + -) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === '-') { - winMain.webContents.zoomFactor -= 0.1; + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === '-' ) + { + winMain.webContents.zoomFactor -= 0.1 } /* Input > Zoom Reset (CTRL + 0) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === '0') { - winMain.webContents.zoomFactor = 1; + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === '0' ) + { + winMain.webContents.zoomFactor = 1 } /* Input > Quit (CTRL + q) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === 'q') { - app.isQuiting = true; - app.quit(); + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === 'q' ) + { + app.isQuiting = true + app.quit() } /* Input > Minimize to tray (CTRL + m) */ - if ((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.type === 'keyDown' && input.control && input.key === 'm') { - bWinHidden = 1; - winMain.hide(); + if ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.type === 'keyDown' && input.control && input.key === 'm' ) + { + bWinHidden = 1 + winMain.hide() } /* Input > Dev Tools (CTRL + SHIFT + I || F12) */ - if (((bHotkeysEnabled == 1 || store.get('bHotkeys') == 1) && input.control && input.shift) || input.key === 'F12') { - if (input.type === 'keyDown' && (input.key === 'I' || input.key === 'F12')) { - winMain.webContents.toggleDevTools(); - winMain.webContents.on('devtools-opened', () => { + if ( ( ( bHotkeysEnabled == 1 || store.get( 'bHotkeys' ) == 1 ) && input.control && input.shift ) || input.key === 'F12' ) + { + if ( input.type === 'keyDown' && ( input.key === 'I' || input.key === 'F12' ) ) + { + winMain.webContents.toggleDevTools() + winMain.webContents.on( 'devtools-opened', () => + { winMain.webContents.devToolsWebContents .executeJavaScript( ` @@ -909,13 +986,14 @@ function ready() { }) ` ) - .then(() => { - winMain.webContents.toggleDevTools(); - }); - }); + .then( () => + { + winMain.webContents.toggleDevTools() + } ) + } ) } } - }); + } ) /* Tray @@ -924,18 +1002,22 @@ function ready() { Linux : left and right click have same functionality */ - tray = new Tray(appIcon); - tray.setToolTip(`${appName}`); - tray.setContextMenu(contextMenu); - tray.on('click', function () { - if (bWinHidden) { - bWinHidden = 0; - winMain.show(); - } else { - bWinHidden = 1; - winMain.hide(); + tray = new Tray( appIcon ) + tray.setToolTip( `${ appName }` ) + tray.setContextMenu( contextMenu ) + tray.on( 'click', function () + { + if ( bWinHidden ) + { + bWinHidden = 0 + winMain.show() + } + else + { + bWinHidden = 1 + winMain.hide() } - }); + } ) /* Loop args @@ -945,16 +1027,24 @@ function ready() { --quit : quit app when close button pressed */ - for (let i = 0; i < process.argv.length; i++) { - if (process.argv[i] === '--hidden') { - bWinHidden = 1; - } else if (process.argv[i] === '--dev') { - bDevTools = 1; + for ( let i = 0; i < process.argv.length; i++ ) + { + if ( process.argv[ i ] === '--hidden' ) + { + bWinHidden = 1 + } + else if ( process.argv[ i ] === '--dev' ) + { + bDevTools = 1 activeDevTools() - } else if (process.argv[i] === '--quit') { - bQuitOnClose = 1; - } else if (process.argv[i] === '--hotkeys') { - bHotkeysEnabled = 1; + } + else if ( process.argv[ i ] === '--quit' ) + { + bQuitOnClose = 1 + } + else if ( process.argv[ i ] === '--hotkeys' ) + { + bHotkeysEnabled = 1 } } @@ -962,8 +1052,8 @@ function ready() { Run timer every X seconds to check for new messages */ - const fetchInterval = ((store.get('pollrate') || _Pollrate) * 1000) + 600; - timerPollrate = setInterval(GetMessages, fetchInterval); + const fetchInterval = ( ( store.get( 'pollrate' ) || _Pollrate ) * 1000 ) + 600 + timerPollrate = setInterval( GetMessages, fetchInterval ) /* Check stored setting for developer tools and set state when @@ -976,12 +1066,12 @@ function ready() { Start minimized in tray */ - if( store.get('bStartHidden') == 1 || bWinHidden == 1) - winMain.hide(); + if ( store.get( 'bStartHidden' ) == 1 || bStartHidden == 1 || bWinHidden == 1 ) + winMain.hide() } /* App > Ready */ -app.on('ready', ready); +app.on( 'ready', ready ) diff --git a/package-lock.json b/package-lock.json index 0320347..4f3f7f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,8 @@ "devDependencies": { "@aetherinox/noxenv": "^1.0.0", "@playwright/test": "^1.45.3", + "@stylistic/eslint-plugin-js": "^2.3.0", + "@stylistic/eslint-plugin-plus": "^2.3.0", "@testing-library/jest-dom": "^6.4.8", "@types/node": "^20.14.11", "@types/testing-library__jest-dom": "^5.14.9", @@ -945,6 +947,70 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz", + "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.3.0.tgz", + "integrity": "sha512-xboPWGUU5yaPlR+WR57GwXEuY4PSlPqA0C3IdNA/+1o2MuBi95XgDJcZiJ9N+aXsqBXAPIpFFb+WQ7QEHo4f7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.12.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -1012,6 +1078,24 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/eslint": { + "version": "8.56.11", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", + "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -1056,6 +1140,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1142,6 +1233,134 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.17.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1279,6 +1498,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", @@ -1864,6 +2093,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2706,6 +2948,36 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3093,6 +3365,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3989,6 +4282,16 @@ "node": ">=10" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -4386,6 +4689,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", @@ -5377,6 +5690,19 @@ "dev": true, "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -5505,6 +5831,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 143a18c..39bba16 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "scripts": { "test": "npx playwright test", "pretest": "npm run lint", - "lint": "eslint index.js", + "lint": "eslint .", "start": "electron .", "package-win": "electron-packager . ntfy-electron --asar --platform=win32 --arch=all --icon=ntfy.ico --overwrite --ignore=\"^/dist|/build|/.github*|/test-*|/tests*|/playwright*|.all-contributorsrc|.editorconfig|.eslintrc|/.git*|.git*|.npm*|.prettier*\" --prune=true --out=build --appCopyright=\"Copyright (c) 2024\" --win32metadata.FileDescription=\"ntfy desktop client with Electron wrapper\" --win32metadata.ProductName=\"ntfy desktop\" --win32metadata.OriginalFilename=\"ntfy-desktop.exe\" --win32metadata.CompanyName=\"https://github.com/xdpirate/ntfy-electron\"", "package-linux": "electron-packager . ntfy-electron --asar --platform=linux --arch=all --icon=ntfy.png --overwrite --ignore=\"^/dist|/build|/.github*|/test-*|/tests*|/playwright*|.all-contributorsrc|.editorconfig|.eslintrc|/.git*|.git*|.npm*|.prettier*\" --prune=true --out=build --appCopyright=\"Copyright (c) 2024\" --win32metadata.FileDescription=\"ntfy desktop client with Electron wrapper\" --win32metadata.ProductName=\"ntfy desktop\" --win32metadata.OriginalFilename=\"ntfy-desktop.exe\" --win32metadata.CompanyName=\"https://github.com/xdpirate/ntfy-electron\"", @@ -79,6 +79,8 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-promise": "^6.4.0", + "@stylistic/eslint-plugin-js": "^2.3.0", + "@stylistic/eslint-plugin-plus": "^2.3.0", "jimp": "^0.22.12", "prettier": "^3.3.3" }, diff --git a/playwright.config.js b/playwright.config.js index bcb5aa3..2072cf0 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,62 +1,62 @@ // @ts-check -const { defineConfig, devices } = require('@playwright/test'); +const { defineConfig, devices } = require( '@playwright/test' ) -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv +/* + Read environment variables from file. + https://github.com/motdotla/dotenv */ + // require('dotenv').config({ path: path.resolve(__dirname, '.env') }); -/** - * @see https://playwright.dev/docs/test-configuration - */ -module.exports = defineConfig({ - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: false, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { +/* + @see https://playwright.dev/docs/test-configuration +*/ + +module.exports = defineConfig( { + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://127.0.0.1:3000', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices[ 'Desktop Chrome' ] } + }, + { + name: 'firefox', + use: { ...devices[ 'Desktop Firefox' ] } + }, + { + name: 'webkit', + use: { ...devices[ 'Desktop Safari' ] } + } - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, /* Test against branded browsers. */ // { @@ -67,13 +67,13 @@ module.exports = defineConfig({ // name: 'Google Chrome', // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, - ], + ] - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, -}); + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +} ) diff --git a/store.js b/store.js deleted file mode 100644 index d34bc32..0000000 --- a/store.js +++ /dev/null @@ -1,45 +0,0 @@ -// Respectfully stolen from Cameron Nokes @ Medium: -// https://medium.com/cameron-nokes/how-to-store-user-data-in-electron-3ba6bf66bc1e -const electron = require('electron'); -const path = require('path'); -const fs = require('fs'); - -class Store { - constructor(opts) { - // Renderer process has to get `app` module via `remote`, whereas the main process can get it directly - // app.getPath('userData') will return a string of the user's app data directory path. - const userDataPath = (electron.app || electron.remote.app).getPath('userData'); - // We'll use the `configName` property to set the file name and path.join to bring it all together as a string - this.path = path.join(userDataPath, opts.configName + '.json'); - console.log(`User Config: ${this.path}`); - this.data = parseDataFile(this.path, opts.defaults); - } - - // This will just return the property on the `data` object - get(key) { - return this.data[key]; - } - - set(key, val) { - this.data[key] = val; - // Wait, I thought using the node.js' synchronous APIs was bad form? - // We're not writing a server so there's not nearly the same IO demand on the process - // Also if we used an async API and our app was quit before the asynchronous write had a chance to complete, - // we might lose that data. Note that in a real app, we would try/catch this. - fs.writeFileSync(this.path, JSON.stringify(this.data)); - } -} - -function parseDataFile(filePath, defaults) { - // We'll try/catch it in case the file doesn't exist yet, which will be the case on the first application run. - // `fs.readFileSync` will return a JSON string which we then parse into a Javascript object - try { - return JSON.parse(fs.readFileSync(filePath)); - } catch(error) { - // if there was some kind of error, return the passed in defaults instead. - return defaults; - } -} - -// expose the class -module.exports = Store; diff --git a/tests/main.spec.js b/tests/main.spec.js index 7b6d2da..d93715b 100644 --- a/tests/main.spec.js +++ b/tests/main.spec.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-console */ // @ts-check /* @@ -14,25 +16,26 @@ Examples available at: https://github.com/spaceagetv/electron-playwright-example */ -const { test, expect, _electron: electron } = require('@playwright/test') -const eph = require('electron-playwright-helpers') +const { test, expect, _electron: electron } = require( '@playwright/test' ) +const eph = require( 'electron-playwright-helpers' ) import jimp from 'jimp' /* Test > ensure ntfy-desktop launches */ -test('launch ntfy-desktop', async () => { - const app = await electron.launch({ +test( 'launch ntfy-desktop', async () => +{ + const app = await electron.launch( { args: [ 'index.js', '--quit' ], env: { ...process.env, - NODE_ENV: 'development', - }, - }); + NODE_ENV: 'development' + } + } ) /* const appInfo = eph.parseElectronApp('./build/ntfy-electron-win32-x64') @@ -40,122 +43,125 @@ test('launch ntfy-desktop', async () => { */ await app.close() -}) +} ) /* Test > full loadup and screenshot */ -test('full load', async () => { +test( 'full load', async () => +{ /* Initialize */ - const app = await electron.launch({ + const app = await electron.launch( { args: [ 'index.js', '--quit' ], env: { ...process.env, - NODE_ENV: 'development', - }, - }); + NODE_ENV: 'development' + } + } ) - const timestamp = Date.now().toString(); + const timestamp = Date.now().toString() - const appPath = await app.evaluate(async ({ app }) => { - return app.getAppPath(); - }); + const appPath = await app.evaluate( async ( { app } ) => + { + return app.getAppPath() + } ) - console.log(appPath); + console.log( appPath ) const window = await app.firstWindow() - console.log(await window.title()); - window.on('console', console.log); + console.log( await window.title() ) + window.on( 'console', console.log ) /* wait for #root div before taking screenshot */ - await window.waitForSelector('#root', { state: 'visible' }); + await window.waitForSelector( '#root', { state: 'visible' } ) /* path: `e2e/screenshots/test-${timestamp}.png`, */ - const ss1 = await window.screenshot({ path: './test-results/1.png' }) + const ss1 = await window.screenshot( { path: './test-results/1.png' } ) /* Since the close button minimizes to tray, activate the menu and select quit */ - await eph.clickMenuItemById(app, 'quit'); + await eph.clickMenuItemById( app, 'quit' ) await app.close() -}) +} ) /* Test functionality */ -test('app usage', async () => { +test( 'app usage', async () => +{ /* Initialize */ - const app = await electron.launch({ + const app = await electron.launch( { args: [ 'index.js', '--quit' ], env: { ...process.env, - NODE_ENV: 'development', - }, - }); + NODE_ENV: 'development' + } + } ) /* test functionality through app */ const page = await app.firstWindow() - await page.getByLabel('Sign in').click(); - await page.getByLabel('Username *').click(); - await page.getByLabel('Username *').fill('testuser'); - await page.getByLabel('Password *').click(); - await page.getByLabel('Password *').fill('123456789'); - await page.getByRole('button', { name: 'Sign in' }).click(); - await page.getByText('Login failed: Invalid').click(); + await page.getByLabel( 'Sign in' ).click() + await page.getByLabel( 'Username *' ).click() + await page.getByLabel( 'Username *' ).fill( 'testuser' ) + await page.getByLabel( 'Password *' ).click() + await page.getByLabel( 'Password *' ).fill( '123456789' ) + await page.getByRole( 'button', { name: 'Sign in' } ).click() + await page.getByText( 'Login failed: Invalid' ).click() /* get expectation */ - expect(page.getByText("Login failed: Invalid")).toBeVisible(); + expect( page.getByText( 'Login failed: Invalid' ) ).toBeVisible() const window = await app.firstWindow() - console.log(await window.title()); - window.on('console', console.log); + console.log( await window.title() ) + window.on( 'console', console.log ) /* wait for #root div before taking screenshot */ - await window.waitForSelector('#root', { state: 'visible' }); + await window.waitForSelector( '#root', { state: 'visible' } ) /* path: `e2e/screenshots/test-${timestamp}.png`, */ - const ss1 = await window.screenshot({ path: './test-results/3.png' }) + const ss1 = await window.screenshot( { path: './test-results/3.png' } ) /* Since the close button minimizes to tray, activate the menu and select quit */ - await eph.clickMenuItemById(app, 'quit'); + await eph.clickMenuItemById( app, 'quit' ) await app.close() -}); +} )