main.js

/**
* @file Contains the main.js code of media-dupes
* @author yafp
* @namespace main
*/

// console.time('init') // start measuring startup time

// -----------------------------------------------------------------------------
// REQUIRE: 3rd PARTY
// -----------------------------------------------------------------------------
const { app, BrowserWindow, electron, ipcMain, Menu } = require('electron')
const shell = require('electron').shell
const path = require('path')
const fs = require('fs')
const openAboutWindow = require('about-window').default // for: about-window

// -----------------------------------------------------------------------------
// Shared object
// -----------------------------------------------------------------------------
//
var consoleOutput = false // can be changed using --verbose

// power save blocker
var powerSaveBlockerEnabled = false
var powerSaveBlockerId = -1

// Settings UI
var enableVerboseMode = false
var enableAdditionalParameter = false
var additionalYoutubeDlParameter = ''
var enableErrorReporting = true
var downloadDir = app.getPath('downloads') // Detect the default-download-folder of the user from the OS
var audioFormat = 'mp3' // mp3 is the default
var confirmedDisclaimer = false

// Main UI
var applicationState = 'idle' // default is idle
var todoListStateEmpty = true // is empty by default

global.sharedObj = {
    // console Output
    consoleOutput: consoleOutput,

    // power management
    powerSaveBlockerEnabled: powerSaveBlockerEnabled,
    powerSaveBlockerId: powerSaveBlockerId,

    // settings UI
    enableErrorReporting: enableErrorReporting,
    enableVerboseMode: enableVerboseMode,
    enableAdditionalParameter: enableAdditionalParameter,
    additionalYoutubeDlParameter: additionalYoutubeDlParameter,
    downloadDir: downloadDir,
    audioFormat: audioFormat,
    confirmedDisclaimer: confirmedDisclaimer,

    // main UI
    applicationState: applicationState,
    todoListStateEmpty: todoListStateEmpty
}

// ----------------------------------------------------------------------------
// REQUIRE: MEDIA-DUPES MODULES
// ----------------------------------------------------------------------------
const crash = require('./app/js/modules/crashReporter.js') // crashReporter
const sentry = require('./app/js/modules/sentry.js') // sentry
const unhandled = require('./app/js/modules/unhandled.js') // electron-unhandled
const utils = require('./app/js/modules/utils.js')

// ----------------------------------------------------------------------------
// COMMAND-LINE-ARGS
// ----------------------------------------------------------------------------

// image to unicode: https://drewish.com/projects/unicoder/
// const appLogo = '\t       ▖▄▖▌▌▌▌▄▖▖       \n\t    ▗▐▐▐▗▚▚▚▚▚▚▚▀▌▌▖    \n\t  ▗▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▚   \n\t ▗▐▐▐▚▚▚▀▝▐▐▐▞▝▝▞▞▞▞▞▌▖ \n\t ▌▌▌▌▌▌▘  ▗▚▜▖  ▝▐▐▞▞▞▞ \n\t▗▚▚▚▌▚    ▐▐▐▖▖   ▌▌▌▌▛▖\n\t▚▚▚▘    ▝▜▐▐▚▚▚▘  ▘▚▚▚▚▘\n\t▚▚▙▘      ▚▚▚▚      ▝▞▞▞\n\t▝▞▄        ▘▘        ▌▌▘\n\t ▌▌▙▖              ▗▐▐▐ \n\t ▝▐▗▚▜▐▚▜▐▚▚▜▐▐▐▐▞▌▌▌▌▘ \n\t  ▝▐▐▐▐▐▐▐▐▐▐▐▐▚▚▚▚▚▘▘  \n\t    ▝▐▐▐▐▐▐▐▐▐▐▐▐▐▝ ▘   \n\t       ▘▘▘▘▚▌▘▘▘▘       '
const appLogo = '       ▖▄▖▌▌▌▌▄▖▖       \n    ▗▐▐▐▗▚▚▚▚▚▚▚▀▌▌▖    \n  ▗▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▚   \n ▗▐▐▐▚▚▚▀▝▐▐▐▞▝▝▞▞▞▞▞▌▖ \n ▌▌▌▌▌▌▘  ▗▚▜▖  ▝▐▐▞▞▞▞ \n▗▚▚▚▌▚    ▐▐▐▖▖   ▌▌▌▌▛▖\n▚▚▚▘    ▝▜▐▐▚▚▚▘  ▘▚▚▚▚▘\n▚▚▙▘      ▚▚▚▚      ▝▞▞▞\n▝▞▄        ▘▘        ▌▌▘\n ▌▌▙▖              ▗▐▐▐ \n ▝▐▗▚▜▐▚▜▐▚▚▜▐▐▐▐▞▌▌▌▌▘ \n  ▝▐▐▐▐▐▐▐▐▐▐▐▐▚▚▚▚▚▘▘  \n    ▝▐▐▐▐▐▐▐▐▐▐▐▐▐▝ ▘   \n       ▘▘▘▘▚▌▘▘▘▘       '

var yargs = require('yargs')
    .strict(true) // arguments must be valid
    .usage('Usage: $0 <command> [options]')
    // define arguments
    .option('verbose', {
        alias: 'v',
        type: 'boolean',
        description: 'Starts media-dupes with verbose output'
    })
    .option('help', {
        alias: 'h',
        type: 'boolean',
        description: 'Shows the media-dupes help'
    })
    .option('version', {
        type: 'boolean',
        description: 'Shows the media-dupes version'
    })

    // displayed in case of invalid parameters
    .showHelpOnFail(false, 'Specify --help to display the available options')

    // show the project url
    .epilog(appLogo)
    .epilog('Project URL: https://github.com/yafp/media-dupes')

    // Show an example
    //
    // .example('$0 --help', 'Shows the media-dupes help')
    .argv

if (yargs.verbose === true) {
    global.sharedObj.consoleOutput = true // update the global object
}

// ----------------------------------------------------------------------------
// ERROR-HANDLING:
// ----------------------------------------------------------------------------
crash.initCrashReporter()
unhandled.initUnhandled()
sentry.enableSentry() // sentry is enabled by default

// ----------------------------------------------------------------------------
// VARIABLES & CONSTANTS
// ----------------------------------------------------------------------------
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
let settingsWindow
let distractionWindow

const gotTheLock = app.requestSingleInstanceLock() // for: single-instance handling
const defaultUserDataPath = app.getPath('userData') // for: storing window position and size

const { urlGitHubGeneral, urlGitHubIssues, urlGitHubChangelog, urlGitHubReleases, urlYoutubeDlSupportedSites } = require('./app/js/modules/urls.js') // project-urls

// Caution: Warning since electron 8
app.allowRendererProcessReuse = true // see: https://github.com/electron/electron/issues/18397. Set to true with media-dupes 0.9.0

// mainWindow: minimal window size
const mainWindowMinimalWindowHeight = 730
const mainWindowMinimalWindowWidth = 600

// settingsWundow: minimal window size
const settingsWindowMinimalWindowHeight = 400
const settingsWindowMinimalWindowWidth = 800

// ----------------------------------------------------------------------------
// FUNCTIONS
// ----------------------------------------------------------------------------

/**
* @function doLog
* @summary Writes console output for the main process
* @description Writes console output for the main process
* @memberof main
* @param {string} type - The log type
* @param {string} message - The log message
*/
function doLog (type, message) {
    const logM = require('electron-log')
    const prefix = '[   Main   ] '

    if (global.sharedObj.consoleOutput === false) {
        logM.transports.console.level = false // disable Terminal output. Still logs to DevTools and LogFile
    }
    // important: https://github.com/megahertz/electron-log/issues/189

    // electron-log can: error, warn, info, verbose, debug, silly
    switch (type) {
    case 'info':
        logM.info(prefix + message)
        break

    case 'warn':
        logM.warn(prefix + message)
        break

    case 'error':
        logM.error(prefix + message)
        break

    default:
        logM.silly(prefix + message)
            // code block
    }
}

/**
* @function createWindowSettings
* @summary Manages the BrowserWindow for the Settings UI
* @description Manages the BrowserWindow for the Settings UI
* @memberof main
*/
function createWindowSettings () {
    doLog('info', 'createWindowSettings ::: Creating the settings window')

    // Create the browser window.for Settings
    settingsWindow = new BrowserWindow({
        modal: true,
        frame: true, // false results in a borderless window. Needed for custom titlebar
        titleBarStyle: 'default', // needed for custom-electron-titlebar. See: https://electronjs.org/docs/api/frameless-window
        backgroundColor: '#ffffff', // since 0.3.0
        show: true, // hide until: ready-to-show
        center: true, // Show window in the center of the screen. (since 0.3.0)
        width: settingsWindowMinimalWindowWidth,
        minWidth: settingsWindowMinimalWindowWidth,
        minimizable: false, // not implemented on linux
        maximizable: false, // not implemented on linux
        height: settingsWindowMinimalWindowHeight,
        minHeight: settingsWindowMinimalWindowHeight,
        icon: path.join(__dirname, 'app/img/icon/icon.png'),
        webPreferences: {
            nodeIntegration: true,
            webSecurity: true // introduced in 0.3.0
        }
    })

    settingsWindow.loadFile('app/settings.html') // load the setting.html to the settings-window
    settingsWindow.removeMenu() // the settings window needs no menu

    // Call from renderer: Settings UI - toggle dev tools
    ipcMain.on('settingsToggleDevTools', function () {
        settingsWindow.webContents.toggleDevTools()
    })

    // Emitted before the window is closed.
    settingsWindow.on('close', function () {
        doLog('info', 'createWindowSettings ::: settingsWindow will close (event: close)')
    })

    // Emitted when the window is closed.
    settingsWindow.on('closed', function (event) {
        doLog('info', 'createWindowSettings ::: settingsWindow is closed (event: closed)')
        settingsWindow = null // Dereference the window object
        mainWindow.webContents.send('unblurMainUI') // unblur the main UI
    })
}

/**
* @function createWindowDistraction
* @summary creates the window for distraction mode
* @description creates the window for distraction mode
* @memberof main
*/
function createWindowDistraction () {
    doLog('info', 'createWindowDistraction ::: Creating the distraction window')

    // Create the distraction browser window.
    distractionWindow = new BrowserWindow({
        frame: true, // false results in a borderless window. Needed for custom titlebar
        backgroundColor: '#ffffff',
        center: true,
        width: 550,
        minWidth: 550,
        resizable: false,
        height: 720,
        minHeight: 720,
        icon: path.join(__dirname, 'app/img/icon/icon.png'),
        webPreferences: {
            webSecurity: true
        }
    })

    distractionWindow.loadFile('app/distraction.html') // load the distraction.html to the
    distractionWindow.removeMenu() // the distraction-window needs no menu

    // Emitted before the window is closed.
    distractionWindow.on('close', function () {
        doLog('info', 'createWindowDistraction ::: distractionWindow will close (event: close)')
    })

    // Emitted when the window is closed.
    distractionWindow.on('closed', function (event) {
        doLog('info', 'createWindowDistraction ::: distractionWindow is closed (event: closed)')
        distractionWindow = null // Dereference the window object
    })
}

/**
* @function createWindowMain
* @summary Creates the mainWindow
* @description Creates the mainWindow (restores window position and size of possible)
* @memberof main
*/
function createWindowMain () {
    doLog('info', 'createWindowMain ::: Starting to create the application windows')

    // Check last window position and size from user data
    var windowWidth
    var windowHeight
    var windowPositionX
    var windowPositionY

    // Read a local config file
    var customUserDataPath = path.join(defaultUserDataPath, 'MediaDupesWindowPosSize.json')
    var data
    try {
        data = JSON.parse(fs.readFileSync(customUserDataPath, 'utf8'))
        windowWidth = data.bounds.width // window size: width
        windowHeight = data.bounds.height // window size: height
        windowPositionX = data.bounds.x // window position: x
        windowPositionY = data.bounds.y // window position: y

        doLog('info', 'createWindowMain ::: Got last window position and size information from _' + customUserDataPath + '_.')
    } catch (e) {
        doLog('warn', 'createWindowMain ::: No last window position and size information found in _' + customUserDataPath + '_. Using fallback values')

        // set some default values for window size
        windowWidth = mainWindowMinimalWindowWidth
        windowHeight = mainWindowMinimalWindowHeight
    }

    // Create the browser window.
    mainWindow = new BrowserWindow({
        frame: false, // false results in a borderless window. Needed for custom titlebar
        titleBarStyle: 'hidden', // needed for custom-electron-titlebar. See: https://electronjs.org/docs/api/frameless-window
        backgroundColor: '#ffffff', // since 0.3.0
        show: false, // hide until: ready-to-show event is fired
        center: true, // Show window in the center of the screen. (since 0.3.0)
        width: windowWidth,
        minWidth: mainWindowMinimalWindowWidth,
        height: windowHeight,
        minHeight: mainWindowMinimalWindowHeight,
        icon: path.join(__dirname, 'app/img/icon/icon.png'),
        webPreferences: {
            enableRemoteModule: true, // since electron 9.0.0
            nodeIntegration: true,
            webSecurity: true // introduced in 0.3.0
        }
    })

    // Restore window position if possible
    //
    // requirements: found values in MediaDupesWindowPosSize.json from the previous session
    if ((typeof windowPositionX !== 'undefined') && (typeof windowPositionY !== 'undefined')) {
        mainWindow.setPosition(windowPositionX, windowPositionY)
    }

    // Call from renderer: reload the application
    ipcMain.on('reloadMainWindow', (event) => {
        doLog('info', 'createWindowMain ::: Trying to reload the main window.')
        mainWindow.reload()
    })

    // Call from renderer: Open download folder
    ipcMain.on('openUserDownloadFolder', (event, userSettingValue) => {
        doLog('info', 'ipc.openUserDownloadFolder ::: Trying to open the user download folder _' + userSettingValue + '_.')

        // try to open it
        shell.openPath(userSettingValue) // FIXME: check result
        /*
        var openUserDownloadFolder = shell.openPath(userSettingValue)
        if (Object.keys(openUserDownloadFolder).length === 0) {
            doLog('info', 'ipc.openUserDownloadFolder :::  Opened the media-dupes download folder (ipcMain)')
        } else {
            doLog('error', 'ipc.openUserDownloadFolder ::: Failed to open the user download folder (ipcMain). Error: ' + openUserDownloadFolder)
        }
        */
    })

    // Call from renderer: Open settings folder
    ipcMain.on('settingsFolderOpen', (event) => {
        doLog('info', 'ipc.settingsFolderOpen ::: Trying to open the users settings folder (ipcMain)')
        const userSettingsPath = path.join(app.getPath('userData'), 'UserSettings') // change path for userSettings

        // try to open it
        shell.openPath(userSettingsPath) // FIXME: check result
        /*
        if (shell.openPath(userSettingsPath) === true) {
            doLog('info', 'ipc.settingsFolderOpen ::: Opened the media-dupes settings folder (ipcMain)')
        } else {
            doLog('error', 'ipc.settingsFolderOpen ::: Failed to open the user settings folder (ipcMain)')
        }
        */
    })

    // Call from renderer:  Urgent window
    ipcMain.on('makeWindowUrgent', function () {
        mainWindow.flashFrame(true)
    })

    // Call from renderer: Option: load settings UI
    ipcMain.on('settingsUiLoad', function () {
        createWindowSettings()
    })

    // Call from renderer: Option: load distraction UI
    ipcMain.on('startDistraction', function () {
        createWindowDistraction()
    })

    // Call from renderer: show mainUI
    ipcMain.on('showAndFocusMainUI', function () {
        mainWindow.show()
        mainWindow.focus()
    })

    // Call from renderer: Update property from globalObj
    ipcMain.on('globalObjectSet', function (event, property, value) {
        doLog('info', 'ipc.globalObjectSet ::: Set _' + property + '_ to: _' + value + '_')
        global.sharedObj[property] = value // update the property in the global shared object
        if (global.sharedObj.consoleOutput === true) { // If console output is enabled- show the entire sharedObject
            console.warn(global.sharedObj)
        }
    })

    // Call from renderer - Enable the power save blocker. See #97
    ipcMain.on('enablePowerSaveBlocker', function () {
        const { powerSaveBlocker } = require('electron')
        const id = powerSaveBlocker.start('prevent-display-sleep')

        if (powerSaveBlocker.isStarted(id)) {
            doLog('info', 'ipc.enablePowerSaveBlocker ::: Successfully enabled the PowerSaveBlocker with the ID _' + id + '_ as app is currently downloading')
            global.sharedObj.powerSaveBlockerEnabled = true
            global.sharedObj.powerSaveBlockerId = id
        } else {
            doLog('error', 'ipc.enablePowerSaveBlocker ::: Enabling the Power-Save-Blocker for the current download failed')
            global.sharedObj.powerSaveBlockerEnabled = false
            global.sharedObj.powerSaveBlockerId = -1
        }
    })

    // Call from renderer: disables the powersaveblocker
    ipcMain.on('disablePowerSaveBlocker', function (event, id) {
        const { powerSaveBlocker } = require('electron')
        powerSaveBlocker.stop(id)
        global.sharedObj.powerSaveBlockerEnabled = false
        global.sharedObj.powerSaveBlockerId = -1
        doLog('info', 'ipc.disablePowerSaveBlocker ::: Disabled the PowerSaveBlocker with the ID: _' + id + '_.')
    })

    mainWindow.loadFile('app/index.html') // load the index.html to the main window

    // Emitted when the web page becomes unresponsive.
    mainWindow.on('unresponsive', function () {
        doLog('warn', 'createWindowMain (on unresponsive) ::: mainWindow is now unresponsive (event: unresponsive)')
    })

    // Emitted when the unresponsive web page becomes responsive again.
    mainWindow.on('responsive', function () {
        doLog('info', 'createWindowMain (on responsive) ::: mainWindow is now responsive again (event: responsive)')
    })

    // Emitted when the web page has been rendered (while not being shown) and window can be displayed without a visual flash.
    mainWindow.on('ready-to-show', function () {
        doLog('info', 'createWindowMain (ready to show) ::: mainWindow is now ready, so show it and then focus it (event: ready-to-show)')
        mainWindow.show()
        mainWindow.focus()

        // do some checks & routines once at start of the application
        mainWindow.webContents.send('startCheckingDependencies') // check application dependencies
        mainWindow.webContents.send('todoListCheck') // search if there are urls to restore
        // mainWindow.webContents.send('unblurMainUI') // unblur the main UI

        // Start checks for updates scheduled to improve startup time
        mainWindow.webContents.send('scheduleUpdateCheckMediaDupes')
        mainWindow.webContents.send('scheduleUpdateCheckYoutubeDl')

        mainWindow.webContents.send('countAppStarts') // count app starts
    })
    // end: ready-toshow

    // Emitted when the web page has been rendered (while not being shown) and window can be displayed without a visual flash.
    mainWindow.on('show', function () {
        doLog('info', 'createWindowMain (show) ::: mainWindow is now ready, so show it and then focus it (event: ready-to-show)')
        mainWindow.webContents.send('blurMainUI') // blur the main UI
        mainWindow.webContents.send('startDisclaimerCheck') // check if disclaimer must be shown

        // console.timeEnd('init') // Stop measuring startup time
    })
    // end: ready-toshow

    // Emitted before the window is closed.
    mainWindow.on('close', function (event) {
        doLog('info', 'createWindowMain (on close) ::: mainWindow will close (event: close)')

        var curState = global.sharedObj.applicationState // get applicationState
        doLog('info', 'createWindowMain (on close) ::: Current application state is: _' + curState + '_.')

        if (curState === 'Download in progress') {
            var choiceA = require('electron').dialog.showMessageBoxSync(this, // since electron7 showMessageBox no longer blocks the close. Therefor we are using showMessageBoxSync
                {
                    icon: path.join(__dirname, 'app/img/icon/icon.png'),
                    type: 'question',
                    buttons: ['Yes', 'No'],
                    title: 'Downloads in progress',
                    message: 'media-dupes is currently downloading.\n\nDo you really want to quit?'
                })
            if (choiceA === 1) {
                event.preventDefault() // user pressed No
                return
            }
        }

        // todoList handling - see #66
        //
        var curTodoListStateEmpty = global.sharedObj.todoListStateEmpty
        doLog('info', 'createWindowMain (on close) ::: Is the todo list currently empty?: _' + curTodoListStateEmpty + '_.')
        if (curTodoListStateEmpty === false) {
            // todo List contains data which should be handled
            var choiceB = require('electron').dialog.showMessageBoxSync(this,
                {
                    icon: path.join(__dirname, 'app/img/icon/icon.png'),
                    type: 'question',
                    buttons: ['Yes', 'No'],
                    title: 'Save current todo list?',
                    message: 'Your todo list contains unprocessed URLs.\n\nDo you want to restore them on next launch?'
                })

            if (choiceB === 0) {
                doLog('info', 'createWindowMain (on close) ::: User wants to save his todo list')
                mainWindow.webContents.send('todoListTryToSave')
            } else {
                doLog('info', 'createWindowMain (on close) ::: User does NOT want to save his todo list')
            }
        } else {
            doLog('info', 'createWindowMain ::: There is nothing in the todo list to save')
        }

        // get window position and size
        var data = {
            bounds: mainWindow.getBounds()
        }

        // define target path (in user data)
        var customUserDataPath = path.join(defaultUserDataPath, 'MediaDupesWindowPosSize.json')

        // try to write
        fs.writeFile(customUserDataPath, JSON.stringify(data), function (error) {
            if (error) {
                doLog('error', 'createWindowMain ::: storing window-position and -size of mainWindow in  _' + customUserDataPath + '_ failed with error: _' + error + '_ (event: close)')
                throw error
            }

            doLog('info', 'createWindowMain ::: mainWindow stored window-position and -size in _' + customUserDataPath + '_ (event: close)')
        })
    })
    // end: close

    // Emitted when the window is closed.
    mainWindow.on('closed', function (event) {
        doLog('info', 'createWindowMain (closed) ::: mainWindow is closed (event: closed)')
        mainWindow = null // Dereference the window object,
    })
    // end: closed
}

/**
* @function createMenuMain
* @summary Creates the menu for the main UI
* @description Creates the menu for the main UI
* @memberof main
*/
function createMenuMain () {
    var menu = Menu.buildFromTemplate([

        // Menu: File
        {
            label: 'File',
            submenu: [
                // Settings
                {
                    label: 'Settings',
                    icon: path.join(__dirname, 'app/img/menu/file/cog_red.png'),
                    click () {
                        mainWindow.webContents.send('openSettings')
                    },
                    accelerator: 'CmdOrCtrl+,'
                },
                {
                    type: 'separator'
                },
                // Exit
                {
                    role: 'quit',
                    label: 'Exit',
                    icon: path.join(__dirname, 'app/img/menu/file/power-off_red.png'),
                    click () {
                        app.quit()
                    },
                    accelerator: 'CmdOrCtrl+Q'
                }
            ]
        },

        // Menu: Edit
        {
            label: 'Edit',
            submenu: [
                {
                    label: 'Undo',
                    icon: path.join(__dirname, 'app/img/menu/edit/undo_red.png'),
                    accelerator: 'CmdOrCtrl+Z',
                    selector: 'undo:'
                },
                {
                    label: 'Redo',
                    icon: path.join(__dirname, 'app/img/menu/edit/redo_red.png'),
                    accelerator: 'Shift+CmdOrCtrl+Z',
                    selector: 'redo:'
                },
                {
                    type: 'separator'
                },
                {
                    label: 'Cut',
                    icon: path.join(__dirname, 'app/img/menu/edit/cut_red.png'),
                    accelerator: 'CmdOrCtrl+X',
                    selector: 'cut:'
                },
                {
                    label: 'Copy',
                    icon: path.join(__dirname, 'app/img/menu/edit/copy_red.png'),
                    accelerator: 'CmdOrCtrl+C',
                    selector: 'copy:'
                },
                {
                    label: 'Paste',
                    icon: path.join(__dirname, 'app/img/menu/edit/paste_red.png'),
                    accelerator: 'CmdOrCtrl+V',
                    selector: 'paste:'
                },
                {
                    label: 'Select All',
                    accelerator: 'CmdOrCtrl+A',
                    selector: 'selectAll:'
                }
            ]
        },

        // Menu: View
        /*
        {
            label: 'View',
            submenu: [
                {
                    role: 'reload',
                    label: 'Reload',
                    click (item, mainWindow) {
                        mainWindow.reload()
                    },
                    accelerator: 'CmdOrCtrl+R'
                }
            ]
        },
        */

        // Menu: Youtube
        /*
        {
            label: 'Search',
            submenu: [
                {
                    label: 'Youtube Suggest',
                    click (item, mainWindow) {
                        mainWindow.webContents.send('openYoutubeSuggestDialog')
                    },
                    accelerator: 'CmdOrCtrl+S'
                }
            ]
        },
        */

        // Menu: Window
        {
            label: 'Window',
            submenu: [
                {
                    role: 'reload',
                    label: 'Reload',
                    click (item, mainWindow) {
                        mainWindow.reload()
                    },
                    accelerator: 'CmdOrCtrl+R'
                },
                {
                    role: 'togglefullscreen',
                    label: 'Toggle Fullscreen',
                    click (item, mainWindow) {
                        if (mainWindow.isFullScreen()) {
                            mainWindow.setFullScreen(false)
                        } else {
                            mainWindow.setFullScreen(true)
                        }
                    },
                    accelerator: 'F11' // is most likely predefined on osx - results in: doesnt work on osx
                },
                {
                    role: 'minimize',
                    label: 'Minimize',
                    icon: path.join(__dirname, 'app/img/menu/window/window-minimize_red.png'),
                    click (item, mainWindow) {
                        if (mainWindow.isMinimized()) {
                            // mainWindow.restore();
                        } else {
                            mainWindow.minimize()
                        }
                    },
                    accelerator: 'CmdOrCtrl+M'
                },
                {
                    label: 'Maximize',
                    icon: path.join(__dirname, 'app/img/menu/window/window-maximize_red.png'),
                    click (item, mainWindow) {
                        if (mainWindow.isMaximized()) {
                            mainWindow.unmaximize()
                        } else {
                            mainWindow.maximize()
                        }
                    },
                    accelerator: 'CmdOrCtrl+K'
                }
            ]
        },

        // Menu: Help
        {
            role: 'help',
            label: 'Help',
            submenu: [
                // About
                {
                    role: 'about',
                    label: 'About',
                    icon: path.join(__dirname, 'app/img/menu/help/info-circle_red.png'),
                    click () {
                        openAboutWindow({
                            icon_path: path.join(__dirname, 'app/img/about/icon_about.png'),
                            open_devtools: false,
                            use_version_info: true,
                            win_options: // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions
                    {
                        autoHideMenuBar: true,
                        titleBarStyle: 'hidden',
                        minimizable: false, // not implemented on linux
                        maximizable: false, // not implemented on linux
                        movable: false, // not implemented on linux
                        resizable: false,
                        alwaysOnTop: true,
                        fullscreenable: false,
                        skipTaskbar: false
                    }
                        })
                    }
                },
                // open homepage
                {
                    label: 'Homepage',
                    icon: path.join(__dirname, 'app/img/menu/help/github_red.png'),
                    click () {
                        shell.openExternal(urlGitHubGeneral)
                    },
                    accelerator: 'F1'
                },
                // report issue
                {
                    label: 'Report issue',
                    icon: path.join(__dirname, 'app/img/menu/help/github_red.png'),
                    click () {
                        shell.openExternal(urlGitHubIssues)
                    },
                    accelerator: 'F2'
                },
                // open changelog
                {
                    label: 'Changelog',
                    icon: path.join(__dirname, 'app/img/menu/help/github_red.png'),
                    click () {
                        shell.openExternal(urlGitHubChangelog)
                    },
                    accelerator: 'F3'
                },
                // open Releases
                {
                    label: 'Releases',
                    icon: path.join(__dirname, 'app/img/menu/help/github_red.png'),
                    click () {
                        shell.openExternal(urlGitHubReleases)
                    },
                    accelerator: 'F4'
                },
                {
                    type: 'separator'
                },
                // Youtube-suggest
                {
                    label: 'Youtube Suggest',
                    click (item, mainWindow) {
                        mainWindow.webContents.send('openYoutubeSuggestDialog')
                    },
                    accelerator: 'CmdOrCtrl+S'
                },
                {
                    type: 'separator'
                },
                // Update
                {
                    label: 'Search media-dupes updates',
                    icon: path.join(__dirname, 'app/img/menu/help/cloud-download-alt_red.png'),
                    click (item, mainWindow) {
                        mainWindow.webContents.send('startSearchUpdatesVerbose')
                    },
                    enabled: true,
                    accelerator: 'F9'
                },
                {
                    type: 'separator'
                },

                // Console
                {
                    id: 'HelpConsole',
                    label: 'Console',
                    icon: path.join(__dirname, 'app/img/menu/help/terminal_red.png'),
                    click (item, mainWindow) {
                        mainWindow.webContents.toggleDevTools()
                    },
                    enabled: true,
                    accelerator: 'F12'
                },
                {
                    type: 'separator'
                },
                // SubMenu youtube-dl maintenance of help
                {
                    label: 'Youtube-DL',
                    icon: path.join(__dirname, 'app/img/menu/help/youtube_red.png'),
                    submenu: [
                        // Show supported sites
                        {
                            id: 'youtubeDlShowSupportedSites',
                            label: 'Show list of supported sites',
                            click () {
                                shell.openExternal(urlYoutubeDlSupportedSites)
                            },
                            enabled: true
                        },
                        // Clear cache in userData
                        {
                            id: 'youtubeDlBinaryPathReset',
                            label: 'Reset youtube-dl binary path',
                            click (item, mainWindow) {
                                mainWindow.webContents.send('youtubeDlBinaryPathReset')
                            },
                            enabled: true
                        },
                        // Force update (ignoring if there is an update available or not)
                        {
                            id: 'youtubeDlBinaryUpdateForce',
                            label: 'Force updating youtube-dl binary',
                            click (item, mainWindow) {
                                mainWindow.webContents.send('youtubeDlBinaryUpdate')
                            },
                            enabled: true
                        }
                    ]
                }
            ]
        }
    ])

    // use the menu
    Menu.setApplicationMenu(menu)
}

/**
* @function forceSingleAppInstance
* @summary Takes care that there is only 1 instance of this app running
* @description Takes care that there is only 1 instance of this app running
* @memberof main
*/
function forceSingleAppInstance () {
    if (!gotTheLock) {
        doLog('warn', 'forceSingleAppInstance ::: There is already another instance of media-dupes')
        app.quit() // quit the second instance
    } else {
        app.on('second-instance', (event, commandLine, workingDirectory) => {
            // Someone tried to run a second instance, we should focus our first instance window.
            // if (mainWindow) {
            if (mainWindow === null) {
                // do nothing - there is no mainwindow - most likely we are on macOS
            } else {
                // mainWindow exists
                if (mainWindow.isMinimized()) {
                    mainWindow.restore()
                }
                mainWindow.focus()
            }
            // }
        })
    }
}

/**
* @function powerMonitorInit
* @summary Initialized a powermonitor after the app is ready
* @description Initialized a powermonitor after the app is ready. See: https://electronjs.org/docs/api/power-monitor
* @memberof main
*/
function powerMonitorInit () {
    const { powerMonitor } = require('electron') // This module cannot be used until the ready event of the app module is emitted.

    // suspend
    powerMonitor.on('suspend', () => {
        doLog('warn', 'powerMonitorInit ::: The system is going to sleep (event: suspend)')
        mainWindow.webContents.send('todoListTryToSave') // try to save the todolist - see #79
        powerMonitorNotify('warning', 'The system is going to sleep (event: suspend)', 0)
    })

    // resume
    powerMonitor.on('resume', () => {
        doLog('info', 'powerMonitorInit ::: The system is resuming (event: resume)')
        mainWindow.webContents.send('todoListCheck') // search if there are urls to restore
        powerMonitorNotify('info', 'The system resumed (event: resume)', 0)
    })

    // shutdown (Linux, macOS)
    powerMonitor.on('shutdown', () => {
        doLog('info', 'powerMonitorInit ::: The system is going to shutdown (event: shutdown)')
    })

    // OTHER SUPPORTED EVENTS:
    //
    // on-ac (Windows)
    // on-battery (Windows)
    // lock-screen (macOs, Windows)
    // unlock-screen (macOs, Windows)
}

/**
* @function powerMonitorNotify
* @summary Used to tell the renderer to display a notification
* @description Used to tell the renderer to display a notification
* @memberof main
*/
function powerMonitorNotify (messageType, messageText, messageDuration) {
    doLog('warn', 'powerMonitorNotify ::: Going to tell the renderer to show a powerMonitor notification')
    mainWindow.webContents.send('powerMonitorNotification', messageType, messageText, messageDuration)
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
//
// app.on('ready', createWindow)
app.on('ready', function () {
    forceSingleAppInstance() // check for single instance
    createWindowMain() // create the application UI
    createMenuMain() // create the application menu
    powerMonitorInit() //
})

// Quit when all windows are closed.
app.on('window-all-closed', function () {
    // On macOS it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (mainWindow === null) {
        createWindowMain()
    }
})