src/index.js
/**
* @file The main file. Controls most things
* @author willyb321
* @copyright MIT
*/
/* eslint-disable no-undef */
/** global: LogWatcher */
import electron, {Menu, dialog, ipcMain, shell} from 'electron';
import path from 'path';
import os from 'os';
import {autoUpdater} from 'electron-updater';
import fs from 'fs-extra';
import tableify from 'tableify';
import _ from 'underscore';
import isDev from 'electron-is-dev';
import jsonfile from 'jsonfile';
import bugsnag from 'bugsnag';
import openAboutWindow from 'about-window';
import storage from 'electron-json-storage';
import moment from 'moment';
import windowStateKeeper from 'electron-window-state';
const app = electron.app;
bugsnag.register('2ec6a43af0f3ef1f61f751191d6bd847', {appVersion: app.getVersion(), sendCode: true});
let win;
/** Autoupdater on update available */
autoUpdater.on('update-available', info => { // eslint-disable-line no-unused-vars
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'New update available.',
message: 'Press OK to download the update, and the application will download the update and then tell you when its done.'
});
win.loadURL(`file:///${__dirname}/index.html`);
});
/** Autoupdater on downloaded */
autoUpdater.on('update-downloaded', (event, info) => { // eslint-disable-line no-unused-vars
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Update ready to install.',
message: 'The update is downloaded, and will be installed on quit. The version downloaded is: ' + event.version
});
});
/** Autoupdater if error */
autoUpdater.on('error', error => {
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Update ready to install.',
message: `Sorry, we've had an error. The message is ` + error
});
if (!isDev && uncaughtErr(error) !== {out: true}) {
bugsnag.notify(error);
}
});
autoUpdater.on('download-progress', percent => {
win.setProgressBar(percent.percent, {mode: 'normal'});
process.mainContents.executeJavaScript(`dlProgress(${Math.round(percent.percent * 100) / 100})`);
});
const stopdrop = `<script>document.addEventListener('dragover', event => event.preventDefault()); document.addEventListener('drop', event => event.preventDefault()); const {ipcRenderer} = require('electron'); document.ondrop=(a=>{a.preventDefault();for(let b of a.dataTransfer.files)ipcRenderer.send("asynchronous-drop",b.path);return!1});</script>`;
const webview = `<webview id="foo" src="${__dirname}/filter.html" style="display:inline-flex; position:fixed; float: right; top:0;" nodeintegration="on"></webview>`;
let JSONParsedEvent = [];
let JSONParsed = []; // eslint-disable-line prefer-const
const logPath = path.join(os.homedir(), 'Saved Games', 'Frontier Developments', 'Elite Dangerous');
const css = '<meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/node_modules/izitoast/dist/css/iziToast.css"><script src="https://use.fontawesome.com/a39359b6f9.js"></script><style>html, body{padding: 0;margin: 0;}#rectangle{width: 100%;height: 100%;background: red;}body{background-color: #313943;color: #bbc8d8;font-family: \'Lato\';font-size: 22px;font-weight: 500;line-height: 36px;margin-bottom: 36px;text-align: center;animation: fadein 0.5s;/* Cover the whole window */height: 100%;/* Make sure this matches the native window background color that you pass to * electron.BrowserWindow({...}), otherwise your app startup will look janky. */background: #313943;}header{position: absolute;width: 500px;height: 250px;top: 50%;left: 50%;margin-top: -125px;margin-left: -250px;text-align: center;}header h1{font-size: 60px;font-weight: 100;margin: 0;padding: 0;}#grad{background: -webkit-linear-gradient(left, #5A3F37, #2C7744);/* For Safari 5.1 to 6.0 */background: -o-linear-gradient(right, #5A3F37, #2C7744);/* For Opera 11.1 to 12.0 */background: -moz-linear-gradient(right, #5A3F37, #2C7744);/* For Firefox 3.6 to 15 */background: linear-gradient(to right, #5A3F37, #2C7744);/* Standard syntax */}hr{display: flex}@keyframes fadein{from{opacity: 0;}to{opacity: 1;}}.app{/* Disable text selection, or your app will feel like a web page */-webkit-user-select: none;-webkit-app-region: drag;/* Cover the whole window */height: 100%;/* Make sure this matches the native window background color that you pass to * electron.BrowserWindow({...}), otherwise your app startup will look janky. */background: #313943;/* Smoother startup */animation: fadein 0.5s;}body::-webkit-scrollbar{width: 1em;}body::-webkit-scrollbar-track{-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);}body::-webkit-scrollbar-thumb{background-color: darkgrey;outline: 1px solid slategrey;}</style><link href="https://fonts.googleapis.com/css?family=Lato:400,400italic,700" rel="stylesheet" type="text/css">';
// adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
// prevent window being garbage collected
let mainWindow;
/**
* @description Makes the main window
*/
function createMainWindow() {
let mainWindowState = windowStateKeeper({ // eslint-disable-line prefer-const
defaultWidth: 600,
defaultHeight: 400
});
win = new electron.BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
backgroundColor: '#313943'
});
mainWindowState.manage(win);
process.mainContents = win.webContents;
win.on('closed', onClosed);
}
/**
* Called by createMainWindow() on closing.
*/
function onClosed() {
// dereference the window
// for multiple windows store them in an array
mainWindow = null;
}
/**
* @description Checks whether user is opted in our out
*/
function opted() {
storage.get('optOut', (err, data) => {
if (err) {
uncaughtErr(err);
}
return !(data.out === false || data.out === undefined);
});
}
/**
* Used by various functions to show a dialog for loading files into the program
*/
function dialogLoad() {
return dialog.showOpenDialog({
defaultPath: logPath,
buttonLabel: 'Load File',
filters: [{
name: 'Logs and saved HTML/JSON',
extensions: ['log', 'html', 'json']
}, {
name: 'All files',
extensions: ['*']
}]
}, {
properties: ['openFile']
});
}
/**
* On any uncaught exception notifys bugsnag and console logs the error.
* @param err - The error.
*/
function uncaughtErr(err) {
storage.get('optOut', (error, data) => {
if (data && !error) {
console.log(data);
if (data.out === false) {
bugsnag.notify(err);
return data;
} else if (data.out === true) {
dialog.showErrorBox('Error!', 'Please report the following: \n' + err);
return data;
}
} else {
console.error(error.stack || error);
return err;
}
});
console.log('ERROR! The error is: ' + err || err.stack);
}
/**
* @description catches uncaught errors.
* @param err - The error.
*/
process.on('uncaughtException', err => {
uncaughtErr(err);
});
/**
* This is used for filtering.
*/
function getChecked() {
ipcMain.on('asynchronous-message', (event, arg) => {
if (arg === 'All Events') {
win.loadURL('data:text/html,' + webview + '<hr>' + stopdrop + css + process.htmlDone); // eslint-disable-line no-useless-concat
} else {
console.log(arg);
process.filteredEvent = arg;
JSONParsedEvent = [];
process.filteredHTML = '';
loadFilter();
process.isFiltered = true;
}
});
ipcMain.on('asynchronous-message-value', (event, arg) => {
process.selectedValue = arg;
console.log(arg);
event.sender.send('asynchronous-reply', arg);
});
}
/**
* Used for sorting and filtering events.
*/
function sortaSorter() {
if (process.logLoaded === true) {
process.filterOpen = true;
const filterList = _.pluck(JSONParsed, 'event');
process.unique = filterList.filter((elem, index, self) => {
return index === self.indexOf(elem);
});
process.unique = process.unique.sort();
global.eventsFilter = {
prop1: process.unique
};
win.loadURL('data:text/html,' + webview + css + '<hr>' + stopdrop + process.htmlDone); // eslint-disable-line no-useless-concat
getChecked();
} else {
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Please load a file first',
message: 'Please load a file before attempting to filter things that don\'t exist'
});
}
}
/**
* Used to populate the JSONParsedEvent array, which is used to load filtered logs.
*/
function findEvents() {
for (let i = 0; i < JSONParsed.length; i++) {
if (JSONParsed[i].event === process.filteredEvent) {
JSONParsedEvent.push(JSONParsed[i]);
}
}
}
/**
* Generates the html from JSONParsedEvent and loads it.
*/
function loadFilter() {
findEvents();
for (let i = 0; i < JSONParsedEvent.length; i++) {
process.filteredHTML += tableify(JSONParsedEvent[i]) + '<hr>'; // eslint-disable-line prefer-const
}
process.filteredHTML = process.filteredHTML.replace('undefined', '');
win.loadURL('data:text/html,' + webview + css + '<hr>' + stopdrop + process.filteredHTML); // eslint-disable-line no-useless-concat
}
/**
* Code thats used to reduce duplication in loading.
*/
function loadInit() {
let html = ''; // eslint-disable-line prefer-const
process.alterateLoad = true;
process.loadfile = dialogLoad();
loadAlternate(process.loadfile, html);
}
/**
* @description Used in loading files to get the extension more efficiently.
* @param fname the filename to process.
* @returns {string} the extension of the file.
*/
function whatLoading(fname) {
return fname.substr((~-fname.lastIndexOf('.') >>> 0) + 2);
}
/**
* @description Figures out how to load the file that was selected in loadInit()
* @param loadFile an array with the full path of the file being loaded.
* @param html
*/
function loadAlternate(loadFile, html) {
if (Array.isArray(loadFile) === true) {
loadFile = loadFile[0];
}
const loadIt = whatLoading(loadFile);
console.log(loadIt);
switch (loadIt) {
case 'json':
loadOutput();
loadFile = '';
break;
case 'log':
lineReader(loadFile, html);
break;
case 'html':
win.loadURL(loadFile);
break;
default:
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Please load a file first',
message: 'Please load a file before attempting to save things that don\'t exist'
});
}
}
/**
* Used to load a file by dropping it on the application
*/
function loadByDrop() {
let html;
JSONParsed = [];
process.loadfile = [];
process.loadfile.push(process.logDropPath);
if ((/\.(json)$/i).test(process.logDropPath)) {
loadOutputDropped();
process.loadfile = '';
process.logDropped = false;
} else if ((/\.(log)$/i).test(process.logDropPath)) {
lineReader(process.loadfile, html);
} else if ((/\.(html)$/i).test(process.loadfile)) {
win.loadURL(process.loadfile);
process.loadfile = '';
}
}
/**
* Used to open journal files in raw form
*/
function rawLog() {
if (Array.isArray(process.loadfile) === true) {
shell.openItem(process.loadfile[0]);
} else if (typeof process.loadfile === 'string') {
shell.openItem(process.loadfile);
} else {
console.log(process.loadfile);
}
}
/**
* Saves a loaded log as HTML
*/
function funcSaveHTML() {
if (process.logLoaded === true) {
dialog.showSaveDialog({
filters: [{
name: 'HTML',
extensions: ['html']
}]
}, fileName => {
if (fileName === undefined) {
console.log('You didn\'t save the file');
return;
}
// fileName is a string that contains the path and filename created in the save file dialog.
if (process.isFiltered === true) {
fs.writeFile(fileName, css + process.filteredHTML, err => {
if (err) {
console.log(err.message);
}
});
} else {
fs.writeFile(fileName, css + process.htmlDone, err => {
if (err) {
console.log(err.message);
}
});
}
});
} else {
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Please load a file first',
message: 'Please load a file before attempting to save things that don\'t exist'
});
}
}
/**
* @description Loads the JSON that the program outputted.
*/
function loadOutput() {
JSONParsed = [];
process.htmlDone = '';
jsonfile.readFile(process.loadfile[0], (err, obj) => {
if (err) {
console.log(err.message);
}
for (const prop in obj) {
if (!obj.hasOwnProperty(prop)) { // eslint-disable-line no-prototype-builtins
// The current property is not a direct property of p
continue;
}
process.htmlDone += tableify(obj[prop]) + '<hr>';
JSONParsed.push(obj[prop]);
}
process.logLoaded = true;
win.loadURL('data:text/html,' + css + '<hr>' + stopdrop + process.htmlDone);
});
}
/**
* @description Loads the JSON outputted by the program if it was dropped.
*/
function loadOutputDropped() {
JSONParsed = [];
process.htmlDone = '';
jsonfile.readFile(process.logDropPath, (err, obj) => {
if (err) {
console.log(err.message);
}
for (const prop in obj) {
if (!obj.hasOwnProperty(prop)) { // eslint-disable-line no-prototype-builtins
// The current property is not a direct property of p
continue;
}
process.htmlDone += tableify(obj[prop]) + '<hr>';
JSONParsed.push(obj[prop]);
}
process.logLoaded = true;
win.loadURL('data:text/html,' + css + '<hr>' + stopdrop + process.htmlDone);
});
}
/**
* @description Used to save loaded file as JSON.
*/
function funcSaveJSON() {
if (process.logLoaded === true) {
dialog.showSaveDialog({
filters: [{
name: 'JSON',
extensions: ['json']
}]
}, fileName => {
if (fileName === undefined) {
console.log('You didn\'t save the file');
return;
}
if (process.isFiltered === true) {
jsonfile.writeFile(fileName, JSONParsedEvent, err => {
console.error(err);
});
} else {
jsonfile.writeFile(fileName, JSONParsed, err => {
console.error(err);
});
}
});
} else {
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Please load a file first',
message: 'Please load a file before attempting to save things that don\'t exist'
});
}
}
/**
* @description New watching code. See lib/log-watcher.js for the info.
* @param stop - if the watching should be stopped.
*/
function watchGood(stop) {
process.logLoaded = true;
const watcher = new LogWatcher(logPath);
process.mainContents.executeJavaScript(`const iziToast = require('./node_modules/izitoast/dist/js/iziToast.js'); iziToast.show({title: 'Loading!', message: 'Please wait!', position: 'bottomCenter', image: 'icon.png', timeout: '10000'})`);
watcher.on('error', err => {
bugsnag.notify(err);
});
watcher.on('finished', () => {
console.log('it stopped');
process.htmlDone = process.htmlDone.replace('undefined', '');
win.loadURL('data:text/html,' + stopdrop + `<script></script>` + process.htmlDone);
process.mainContents.on('did-finish-load', () => {
fs.readFile(path.join(__dirname, 'index.css'), 'utf-8', (err, data) => {
if (!err) {
const formattedData = data.replace(/\s{2,10}/g, ' ').trim();
process.mainContents.insertCSS(formattedData);
process.mainContents.executeJavaScript(`function scroll() {window.scrollTo(0, document.body.scrollHeight || document.documentElement.scrollHeight);} scroll()`);
} else if (err) {
uncaughtErr(err);
}
});
});
});
watcher.on('stopped', () => {
console.log('nah its stopped');
});
watcher.on('data', obs => {
obs.forEach(ob => {
const {timestamp, event} = ob;
JSONParsed.push('\n' + event, timestamp); // eslint-disable-line no-useless-concat
process.htmlDone += '<hr>' + tableify(event + ` @ ${moment(timestamp).format('h:mm a - D/M ')}` + '<br>'); // eslint-disable-line no-useless-concat
// console.log('\n' + timestamp, event);
delete ob.timestamp;
delete ob.event;
Object.keys(ob).forEach(k => {
if (k.endsWith('_Localised') || !ob[k].toString().startsWith('$')) {
if (k === 'StarPos') {
process.htmlDone += '(x / y / z) <br>' + tableify(ob[k].join('<br>')) + '<br>';
} else if (k === 'Systems') {
process.htmlDone += 'Systems Sold: <br>' + tableify(ob[k].join('<br>')) + '<br>';
} else if (k === 'Materials') {
let objtoarr = _.pairs(ob[k]); // eslint-disable-line prefer-const
let objtoarrmerged = [].concat.apply([], objtoarr);
objtoarrmerged = _.each(objtoarrmerged, (element, index, list) => {
if (!isNaN(parseInt(element, 0))) {
list[index] = element.toString() + '% <br>';
}
});
process.htmlDone += k + ':<br>' + tableify(_.flatten(objtoarrmerged).join(' '));
} else if (typeof ob[k] === 'object') {
let objtoarr = _.pairs(ob[k]); // eslint-disable-line prefer-const
const objtoarrmerged = [].concat.apply([], objtoarr);
process.htmlDone += k + ':<br>' + tableify(_.flatten(objtoarrmerged).join(' <br>')) + ' <br> ';
} else if (k === 'Ingredients') {
let objtoarr = _.pairs(ob[k]); // eslint-disable-line prefer-const
let objtoarrmerged = [].concat.apply([], objtoarr); // eslint-disable-line prefer-const
process.htmlDone += k + ':<br>' + tableify(_.flatten(objtoarrmerged).join(' <br>')) + ' <br> ';
} else {
process.htmlDone += tableify(k) + ': ' + tableify(ob[k]) + '<br>';
console.log('\t' + k, ob[k]);
JSONParsed.push(k + '\n' + ob[k]);
}
}
});
});
});
if (stop === 1) {
watcher.stop();
process.logLoaded = false;
}
}
/**
* Called when all windows are closed.
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
/**
* Makes the main window.
*/
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
/**
* Called when a file is dropped.
*/
ipcMain.on('asynchronous-drop', (event, arg) => {
process.logDropPath = '';
console.log(arg);
process.logDropPath = arg;
process.logDropped = true;
loadByDrop();
process.logDropped = false;
console.log('waddup');
});
/**
* Called when app is ready, and checks for updates.
*/
app.on('ready', () => {
opted();
mainWindow = createMainWindow();
fs.ensureDir(logPath, err => {
if (err) {
console.log(err);
}
});
win.loadURL(`file:///${__dirname}/index.html`);
// watchGood();
if (!isDev && process.env.NODE_ENV !== 'test') {
autoUpdater.checkForUpdates();
}
});
/**
* Menu constructor
* @type {Array}
*/
const template = [{
label: 'File',
submenu: [{
label: 'Save as HTML',
accelerator: 'CmdOrCtrl+S',
click: funcSaveHTML
}, {
label: 'Save as JSON',
accelerator: 'CmdOrCtrl+Shift+S',
click: funcSaveJSON
}, {
label: 'Load',
accelerator: 'CmdOrCtrl+O',
click: loadInit
}, {
label: 'Watch logs',
accelerator: 'CmdOrCtrl+L',
type: 'checkbox',
id: 'checked',
click(checked) {
const stop = 1;
console.log(checked.checked);
if (checked.checked === true) {
watchGood();
} else if (checked.checked === false) {
watchGood(stop);
}
}
}, {
label: 'Open raw log',
click: rawLog
}]
}, {
label: 'Filtering',
submenu: [{
label: 'Filter for:',
accelerator: 'CmdOrCtrl+F',
click: sortaSorter
}]
}, {
label: 'Edit',
submenu: [{
role: 'selectall'
}]
}, {
label: 'View',
submenu: [{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(focusedWindow) {
if (focusedWindow) {
win.reload();
}
}
}, {
role: 'togglefullscreen'
}]
}, {
role: 'window',
submenu: [{
role: 'minimize'
}, {
role: 'close'
}]
}, {
role: 'help',
submenu: [{
label: 'Learn More about Electron',
click() {
shell.openExternal('http://electron.atom.io');
}
}, {
label: 'The Github Repo',
click() {
shell.openExternal('https://github.com/willyb321/elite-journal');
}
}, {
label: 'What Version am I on?',
click() {
dialog.showMessageBox({
type: 'info',
buttons: [],
title: 'Please load a file first',
message: 'Current Version: ' + app.getVersion()
});
}
}, {
label: 'About',
click: () => openAboutWindow({
icon_path: path.join(__dirname, 'icon.png'), // eslint-disable-line camelcase
bug_report_url: 'https://github.com/willyb321/elite-journal/issues', // eslint-disable-line camelcase
homepage: 'https://github.com/willyb321/elite-journal'
})
}, {
label: 'Opt-out of auto crash reporting.',
type: 'checkbox',
id: 'optout',
checked: opted,
click: optout => {
let yes;
if (optout.checked === false) {
yes = 0;
optOut(yes);
} else if (optout.checked === true) {
yes = 1;
optOut(yes);
}
}
}
]
}];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);