<commit_before>/**
 * TagSpaces - universal file and folder organizer
 * Copyright (C) 2017-present TagSpaces UG (haftungsbeschraenkt)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License (version 3) as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * @flow
 */

import { app, BrowserWindow, ipcMain, globalShortcut, dialog } from 'electron';
import windowStateKeeper from 'electron-window-state';
import path from 'path';

let mainWindow = null;
global.splashWorkerWindow = null;

if (process.env.NODE_ENV === 'production') {
  const sourceMapSupport = require('source-map-support');
  sourceMapSupport.install();
  console.log = () => {};
  console.time = () => {};
  console.timeEnd = () => {};
}

let debugMode;
let startupFilePath;
let portableMode;

process.argv.forEach((arg, count) => {
  if (arg.toLowerCase() === '-d' || arg.toLowerCase() === '--debug') {
    debugMode = true;
  } else if (arg.toLowerCase() === '-p' || arg.toLowerCase() === '--portable') {
    app.setPath('userData', process.cwd() + '/tsprofile'); // making the app portable
    portableMode = true;
  } else if (arg.indexOf('-psn') >= 0) { // ignoring the -psn process serial number parameter on MacOS by double click
    arg = '';
  } else if (arg === 'babel-register' || arg === '.' || count === 0) { // ignoring the first argument
    // Ignore these argument
  } else if (arg.length > 2) {
    // console.warn('Opening file: ' + arg);
    if (arg !== './app/main.dev.js' && arg !== './app/') {
      startupFilePath = arg;
    }
  }

  if (portableMode) {
    startupFilePath = undefined;
  }
});

if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
  require('electron-debug')({ showDevTools: false, devToolsMode: 'right' });
  const p = path.join(__dirname, '..', 'app', 'node_modules');
  require('module').globalPaths.push(p);
}

// if (process.platform === 'linux') {
//   app.commandLine.appendSwitch('disable-gpu'); // Fix the freezing the app with a black box on dnd https://github.com/electron/electron/issues/12820
// }

// app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096'); // disabled due crashes on win 7
app.commandLine.appendSwitch('--disable-http-cache');

const installExtensions = async () => {
  const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = require('electron-devtools-installer');

  const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
  const extensions = [
    REACT_DEVELOPER_TOOLS,
    REDUX_DEVTOOLS
  ];

  return Promise
    .all(extensions.map(name => installExtension(name.id, forceDownload)))
    .catch(console.log);
};

app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); // Fix broken autoplay functionality in the av player

app.on('window-all-closed', () => {
  // Respect the OSX convention of having the application in memory even
  // after all windows have been closed
  if (process.platform !== 'darwin') {
    globalShortcut.unregisterAll();
    app.quit();
  }
});

app.on('ready', async () => {
  let workerDevMode = false;
  let mainHTML = `file://${__dirname}/app.html`;

  if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
    await installExtensions();
  }

  if (process.env.NODE_ENV === 'development') {
    workerDevMode = true;
    mainHTML = `file://${__dirname}/appd.html`;
  }

  const mainWindowState = windowStateKeeper({
    defaultWidth: 1280,
    defaultHeight: 800
  });

  function createSplashWorker() {
    // console.log('Dev ' + process.env.NODE_ENV + ' worker ' + showWorkerWindow);
    global.splashWorkerWindow = new BrowserWindow({
      show: workerDevMode,
      x: 0,
      y: 0,
      width: workerDevMode ? 800 : 1,
      height: workerDevMode ? 600 : 1,
      frame: false,
      webPreferences: {
        nodeIntegration: true
      }
    });

    global.splashWorkerWindow.loadURL(`file://${__dirname}/splash.html`);
  }

  createSplashWorker();

  let startupParameter = '';
  if (startupFilePath) {
    if (startupFilePath.startsWith('./') || startupFilePath.startsWith('.\\')) {
      startupParameter = '?open=' + encodeURIComponent(path.join(__dirname, startupFilePath));
    } else {
      startupParameter = '?open=' + encodeURIComponent(startupFilePath);
    }
  }

  mainWindow = new BrowserWindow({
    show: true,
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    icon: path.join(__dirname, 'assets/icons/128x128.png'),
    webPreferences: {
      nodeIntegration: true,
      webviewTag: true
    }
  });

  mainWindow.loadURL(mainHTML + startupParameter);
  mainWindow.setAutoHideMenuBar(true);
  mainWindow.setMenuBarVisibility(false);

  mainWindow.webContents.on('did-finish-load', () => {
    if (!mainWindow) {
      throw new Error('"mainWindow" is not defined');
    }
    // mainWindow.show();
    global.splashWorkerWindow.hide(); // Comment for easy debugging of the worker
    mainWindow.focus();
  });

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
    try {
      global.splashWorkerWindow.close();
      global.splashWorkerWindow = null;
    } catch (err) {
      // console.warn('Error closing the splash window. ' + err);
    }
  });

  mainWindow.webContents.on('crashed', () => {
    const options = {
      type: 'info',
      title: 'Renderer Process Crashed',
      message: 'This process has crashed.',
      buttons: ['Reload', 'Close']
    };
    dialog.showMessageBox(mainWindow, options, (index) => {
      mainWindow.hide();
      if (index === 0) {
        reloadApp();
      } else {
        mainWindow.close();
        globalShortcut.unregisterAll();
      }
    });
  });

  global.splashWorkerWindow.webContents.on('crashed', () => {
    try {
      global.splashWorkerWindow.close();
      global.splashWorkerWindow = null;
    } catch (err) {
      console.warn('Error closing the splash window. ' + err);
    }
    createSplashWorker();
  });

  ipcMain.on('worker', (event, arg) => {
    // console.log('worker event in main.' + arg.result.length);
    if (mainWindow) {
      mainWindow.webContents.send(arg.id, arg);
    }
  });

  ipcMain.on('setSplashVisibility', (event, arg) => { // worker window needed to be visible for the PDF tmb generation
    // console.log('worker event in main: ' + arg.visibility);
    if (global.splashWorkerWindow && arg.visibility) {
      global.splashWorkerWindow.show();
      // arg.visibility ? global.splashWorkerWindow.show() : global.splashWorkerWindow.hide();
    }
  });

  ipcMain.on('app-data-path-request', (event) => {
    event.returnValue = app.getPath('appData');
  });

  ipcMain.on('app-version-request', (event) => {
    event.returnValue = app.getVersion();
  });

  ipcMain.on('app-dir-path-request', (event) => {
    event.returnValue = path.join(__dirname, '');
  });

  ipcMain.on('global-shortcuts-enabled', (e, globalShortcutsEnabled) => {
    if (globalShortcutsEnabled) {
      globalShortcut.register('CommandOrControl+Shift+P', resumePlayback);
      globalShortcut.register('MediaPlayPause', resumePlayback);
      globalShortcut.register('CommandOrControl+Shift+N', newTextFile);
      globalShortcut.register('CommandOrControl+Shift+D', getNextFile);
      globalShortcut.register('MediaNextTrack', getNextFile);
      globalShortcut.register('CommandOrControl+Shift+A', getPreviousFile);
      globalShortcut.register('MediaPreviousTrack', getPreviousFile);
      globalShortcut.register('CommandOrControl+Shift+W', showTagSpaces);
    } else {
      globalShortcut.unregisterAll();
    }
  });

  ipcMain.on('relaunch-app', reloadApp);

  ipcMain.on('quit-application', () => {
    globalShortcut.unregisterAll();
    app.quit();
  });

  process.on('uncaughtException', (error) => {
    if (error.stack) {
      console.error('error:', error.stack);
      throw new Error('error:', error.stack);
    }
    reloadApp();
  });

  mainWindowState.manage(mainWindow);

  function showTagSpaces() {
    if (mainWindow) {
      mainWindow.restore();
      mainWindow.show();
    }
    // mainWindow.webContents.send("showing-tagspaces", "tagspaces");
  }

  function newTextFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'new-text-file');
    }
  }

  function getNextFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'next-file');
    }
  }

  function getPreviousFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'previous-file');
    }
  }

  function resumePlayback() {
    if (mainWindow) {
      mainWindow.webContents.send('play-pause', true);
    }
  }

  function reloadApp() {
    if (mainWindow) {
      mainWindow.loadURL(indexPath);
    }
  }
});

<commit_msg>quiting app on window close under mac os<commit_after>/**
 * TagSpaces - universal file and folder organizer
 * Copyright (C) 2017-present TagSpaces UG (haftungsbeschraenkt)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License (version 3) as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * @flow
 */

import { app, BrowserWindow, ipcMain, globalShortcut, dialog } from 'electron';
import windowStateKeeper from 'electron-window-state';
import path from 'path';

let mainWindow = null;
global.splashWorkerWindow = null;

if (process.env.NODE_ENV === 'production') {
  const sourceMapSupport = require('source-map-support');
  sourceMapSupport.install();
  console.log = () => {};
  console.time = () => {};
  console.timeEnd = () => {};
}

let debugMode;
let startupFilePath;
let portableMode;

process.argv.forEach((arg, count) => {
  if (arg.toLowerCase() === '-d' || arg.toLowerCase() === '--debug') {
    debugMode = true;
  } else if (arg.toLowerCase() === '-p' || arg.toLowerCase() === '--portable') {
    app.setPath('userData', process.cwd() + '/tsprofile'); // making the app portable
    portableMode = true;
  } else if (arg.indexOf('-psn') >= 0) { // ignoring the -psn process serial number parameter on MacOS by double click
    arg = '';
  } else if (arg === 'babel-register' || arg === '.' || count === 0) { // ignoring the first argument
    // Ignore these argument
  } else if (arg.length > 2) {
    // console.warn('Opening file: ' + arg);
    if (arg !== './app/main.dev.js' && arg !== './app/') {
      startupFilePath = arg;
    }
  }

  if (portableMode) {
    startupFilePath = undefined;
  }
});

if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
  require('electron-debug')({ showDevTools: false, devToolsMode: 'right' });
  const p = path.join(__dirname, '..', 'app', 'node_modules');
  require('module').globalPaths.push(p);
}

// if (process.platform === 'linux') {
//   app.commandLine.appendSwitch('disable-gpu'); // Fix the freezing the app with a black box on dnd https://github.com/electron/electron/issues/12820
// }

// app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096'); // disabled due crashes on win 7
app.commandLine.appendSwitch('--disable-http-cache');

const installExtensions = async () => {
  const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = require('electron-devtools-installer');

  const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
  const extensions = [
    REACT_DEVELOPER_TOOLS,
    REDUX_DEVTOOLS
  ];

  return Promise
    .all(extensions.map(name => installExtension(name.id, forceDownload)))
    .catch(console.log);
};

app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required'); // Fix broken autoplay functionality in the av player

app.on('window-all-closed', () => {
  globalShortcut.unregisterAll();
  app.quit();
});

app.on('ready', async () => {
  let workerDevMode = false;
  let mainHTML = `file://${__dirname}/app.html`;

  if (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true') {
    await installExtensions();
  }

  if (process.env.NODE_ENV === 'development') {
    workerDevMode = true;
    mainHTML = `file://${__dirname}/appd.html`;
  }

  const mainWindowState = windowStateKeeper({
    defaultWidth: 1280,
    defaultHeight: 800
  });

  function createSplashWorker() {
    // console.log('Dev ' + process.env.NODE_ENV + ' worker ' + showWorkerWindow);
    global.splashWorkerWindow = new BrowserWindow({
      show: workerDevMode,
      x: 0,
      y: 0,
      width: workerDevMode ? 800 : 1,
      height: workerDevMode ? 600 : 1,
      frame: false,
      webPreferences: {
        nodeIntegration: true
      }
    });

    global.splashWorkerWindow.loadURL(`file://${__dirname}/splash.html`);
  }

  createSplashWorker();

  let startupParameter = '';
  if (startupFilePath) {
    if (startupFilePath.startsWith('./') || startupFilePath.startsWith('.\\')) {
      startupParameter = '?open=' + encodeURIComponent(path.join(__dirname, startupFilePath));
    } else {
      startupParameter = '?open=' + encodeURIComponent(startupFilePath);
    }
  }

  mainWindow = new BrowserWindow({
    show: true,
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    icon: path.join(__dirname, 'assets/icons/128x128.png'),
    webPreferences: {
      nodeIntegration: true,
      webviewTag: true
    }
  });

  mainWindow.loadURL(mainHTML + startupParameter);
  mainWindow.setAutoHideMenuBar(true);
  mainWindow.setMenuBarVisibility(false);

  mainWindow.webContents.on('did-finish-load', () => {
    if (!mainWindow) {
      throw new Error('"mainWindow" is not defined');
    }
    // mainWindow.show();
    global.splashWorkerWindow.hide(); // Comment for easy debugging of the worker
    mainWindow.focus();
  });

  // Emitted when the window is closed.
  mainWindow.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
    try {
      global.splashWorkerWindow.close();
      global.splashWorkerWindow = null;
    } catch (err) {
      // console.warn('Error closing the splash window. ' + err);
    }
  });

  mainWindow.webContents.on('crashed', () => {
    const options = {
      type: 'info',
      title: 'Renderer Process Crashed',
      message: 'This process has crashed.',
      buttons: ['Reload', 'Close']
    };
    dialog.showMessageBox(mainWindow, options, (index) => {
      mainWindow.hide();
      if (index === 0) {
        reloadApp();
      } else {
        mainWindow.close();
        globalShortcut.unregisterAll();
      }
    });
  });

  global.splashWorkerWindow.webContents.on('crashed', () => {
    try {
      global.splashWorkerWindow.close();
      global.splashWorkerWindow = null;
    } catch (err) {
      console.warn('Error closing the splash window. ' + err);
    }
    createSplashWorker();
  });

  ipcMain.on('worker', (event, arg) => {
    // console.log('worker event in main.' + arg.result.length);
    if (mainWindow) {
      mainWindow.webContents.send(arg.id, arg);
    }
  });

  ipcMain.on('setSplashVisibility', (event, arg) => { // worker window needed to be visible for the PDF tmb generation
    // console.log('worker event in main: ' + arg.visibility);
    if (global.splashWorkerWindow && arg.visibility) {
      global.splashWorkerWindow.show();
      // arg.visibility ? global.splashWorkerWindow.show() : global.splashWorkerWindow.hide();
    }
  });

  ipcMain.on('app-data-path-request', (event) => {
    event.returnValue = app.getPath('appData');
  });

  ipcMain.on('app-version-request', (event) => {
    event.returnValue = app.getVersion();
  });

  ipcMain.on('app-dir-path-request', (event) => {
    event.returnValue = path.join(__dirname, '');
  });

  ipcMain.on('global-shortcuts-enabled', (e, globalShortcutsEnabled) => {
    if (globalShortcutsEnabled) {
      globalShortcut.register('CommandOrControl+Shift+P', resumePlayback);
      globalShortcut.register('MediaPlayPause', resumePlayback);
      globalShortcut.register('CommandOrControl+Shift+N', newTextFile);
      globalShortcut.register('CommandOrControl+Shift+D', getNextFile);
      globalShortcut.register('MediaNextTrack', getNextFile);
      globalShortcut.register('CommandOrControl+Shift+A', getPreviousFile);
      globalShortcut.register('MediaPreviousTrack', getPreviousFile);
      globalShortcut.register('CommandOrControl+Shift+W', showTagSpaces);
    } else {
      globalShortcut.unregisterAll();
    }
  });

  ipcMain.on('relaunch-app', reloadApp);

  ipcMain.on('quit-application', () => {
    globalShortcut.unregisterAll();
    app.quit();
  });

  process.on('uncaughtException', (error) => {
    if (error.stack) {
      console.error('error:', error.stack);
      throw new Error('error:', error.stack);
    }
    reloadApp();
  });

  mainWindowState.manage(mainWindow);

  function showTagSpaces() {
    if (mainWindow) {
      mainWindow.restore();
      mainWindow.show();
    }
    // mainWindow.webContents.send("showing-tagspaces", "tagspaces");
  }

  function newTextFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'new-text-file');
    }
  }

  function getNextFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'next-file');
    }
  }

  function getPreviousFile() {
    if (mainWindow) {
      mainWindow.show();
      mainWindow.webContents.send('file', 'previous-file');
    }
  }

  function resumePlayback() {
    if (mainWindow) {
      mainWindow.webContents.send('play-pause', true);
    }
  }

  function reloadApp() {
    if (mainWindow) {
      mainWindow.loadURL(indexPath);
    }
  }
});

<|endoftext|>


<commit_before>
module.exports = function(gulp, config) {

	return function(done) {
		if(!config.templates) return done()
		var c = require('better-console')
		c.info('~ templates')

		var _ = require('lodash')
		var cache = require('gulp-cached')
		var data = require('gulp-data')
		var frontMatter = require('gulp-front-matter')
		var gulpFilter = require('gulp-filter')
		var gulpif = require('gulp-if')
		var gulpJade = require('gulp-jade')
		var jade = require('jade')
		var jadeInheritance = require('gulp-jade-inheritance')
		var jf = require('jsonfile')
		var path = require('path')
		var plumber = require('gulp-plumber')

		var filterJade = gulpFilter('**/*.jade', { restore: true })
		var filterStatic = gulpFilter('**/*.{html,htm}', { restore: true })

		var templatesData = {}

		// Read data from external JSON files
		var resolveData = function(value) {
			var resolve = null
			if(typeof value == 'string' && ~value.indexOf('.json')){
				resolve = jf.readFileSync(path.resolve(config.dir, value), { throws: false })
				if(resolve === null) {
					c.error('Error in JSON data parsing for file ' + value)
				}
			}
			return resolve
		}

		if(config.data) {
			for(key in config.data){
				var value = config.data[key]

				// Try to read data from external JSON files
				var resolve = resolveData(value)
				if(resolve !== null) value = resolve

				templatesData[key] = value
			}
		}

		// Default Jade options
		var jadeOptions = {
			jade: jade,
			pretty: true,
			cache: true,
			doctype: 'html'
		}
		if(config.jade) {
			_.assign(jadeOptions, config.jade)
		}

		var task = gulp.src(config.templates)

		// Init plumber in devmode
		if(config.devmode){
			task = task.pipe(plumber({ errorHandler: require('../helpers/error_handler') }))
		}

		task = task.pipe(frontMatter({
					property: 'frontMatter',
					remove: true
			}))
			.pipe(data(function(file) {
				var name = path.basename(file.path)
				var data = _.cloneDeep(templatesData)

				// In case of name == key, assign value as global object
				if(typeof templatesData[name] !== 'undefined') {
					return _.assign(data, templatesData[name], file.frontMatter)
				} else {
					return _.assign(data, file.frontMatter)
				}
			}))
			.pipe(filterJade)
			.pipe(gulpif(config._lastChanged, jadeInheritance({ basedir: config.dir })))
			.pipe(gulpJade(jadeOptions))
			.pipe(filterJade.restore)
			.pipe(cache('templates', { optimizeMemory: true }))

		return task.pipe(gulp.dest(config.dist_folder))
	}
}
<commit_msg>pass `devmode` and `production` as global variables in templates<commit_after>
module.exports = function(gulp, config) {

	return function(done) {
		if(!config.templates) return done()
		var c = require('better-console')
		c.info('~ templates')

		var _ = require('lodash')
		var cache = require('gulp-cached')
		var data = require('gulp-data')
		var frontMatter = require('gulp-front-matter')
		var gulpFilter = require('gulp-filter')
		var gulpif = require('gulp-if')
		var gulpJade = require('gulp-jade')
		var jade = require('jade')
		var jadeInheritance = require('gulp-jade-inheritance')
		var jf = require('jsonfile')
		var path = require('path')
		var plumber = require('gulp-plumber')

		var filterJade = gulpFilter('**/*.jade', { restore: true })
		var filterStatic = gulpFilter('**/*.{html,htm}', { restore: true })

		var templatesData = {}

		// Read data from external JSON files
		var resolveData = function(value) {
			var resolve = null
			if(typeof value == 'string' && ~value.indexOf('.json')){
				resolve = jf.readFileSync(path.resolve(config.dir, value), { throws: false })
				if(resolve === null) {
					c.error('Error in JSON data parsing for file ' + value)
				}
			}
			return resolve
		}

		if(config.data) {
			for(key in config.data){
				var value = config.data[key]

				// Try to read data from external JSON files
				var resolve = resolveData(value)
				if(resolve !== null) value = resolve

				templatesData[key] = value
			}
		}

		// Default Jade options
		var jadeOptions = {
			jade: jade,
			pretty: true,
			cache: true,
			doctype: 'html'
		}
		if(config.jade) {
			_.assign(jadeOptions, config.jade)
		}

		var task = gulp.src(config.templates)

		// Init plumber in devmode
		if(config.devmode){
			task = task.pipe(plumber({ errorHandler: require('../helpers/error_handler') }))
		}

		templatesData.devmode = config.devmode;
		templatesData.production = !config.devmode;

		task = task.pipe(frontMatter({
					property: 'frontMatter',
					remove: true
			}))
			.pipe(data(function(file) {
				var name = path.basename(file.path)
				var data = _.cloneDeep(templatesData)

				// In case of name == key, assign value as global object
				if(typeof templatesData[name] !== 'undefined') {
					return _.assign(data, templatesData[name], file.frontMatter)
				} else {
					return _.assign(data, file.frontMatter)
				}
			}))
			.pipe(filterJade)
			.pipe(gulpif(config._lastChanged, jadeInheritance({ basedir: config.dir })))
			.pipe(gulpJade(jadeOptions))
			.pipe(filterJade.restore)
			.pipe(cache('templates', { optimizeMemory: true }))

		return task.pipe(gulp.dest(config.dist_folder))
	}
}
<|endoftext|>


<commit_before>const moment               = require('moment');
const Tick                 = require('./tick');
const updatePurchaseStatus = require('./update_values').updatePurchaseStatus;
const ViewPopupUI          = require('../user/view_popup/view_popup.ui');
const BinarySocket         = require('../../base/socket');
const CommonFunctions      = require('../../../_common/common_functions');
const localize             = require('../../../_common/localize').localize;

const TickDisplay = (() => {
    let number_of_ticks,
        display_symbol,
        contract_start_ms,
        contract_category,
        set_barrier,
        barrier,
        abs_barrier,
        display_decimals,
        show_contract_result,
        contract_sentiment,
        price,
        payout,
        ticks_needed,
        x_indicators,
        chart,
        Highcharts,
        applicable_ticks,
        contract_barrier,
        contract_start_moment,
        counter,
        spots_list,
        tick_underlying,
        tick_count,
        tick_longcode,
        tick_display_name,
        tick_date_start,
        absolute_barrier,
        tick_shortcode,
        tick_init,
        subscribe,
        responseID;

    const initialize = (data, options) => {
        // setting up globals
        number_of_ticks      = parseInt(data.number_of_ticks);
        display_symbol       = data.display_symbol;
        contract_start_ms    = parseInt(data.contract_start) * 1000;
        contract_category    = data.contract_category;
        set_barrier          = !contract_category.match('digits');
        barrier              = data.barrier;
        abs_barrier          = data.abs_barrier;
        display_decimals     = data.display_decimals || 2;
        show_contract_result = data.show_contract_result;

        if (data.show_contract_result) {
            contract_sentiment = data.contract_sentiment;
            price              = parseFloat(data.price);
            payout             = parseFloat(data.payout);
        }

        const minimize = data.show_contract_result;
        const end_time = parseInt(data.contract_start) + parseInt((number_of_ticks + 2) * 5);

        setXIndicators();
        CommonFunctions.requireHighstock((Highstock) => {
            Highcharts = Highstock;
            initializeChart({
                minimize,
                plot_from: data.previous_tick_epoch * 1000,
                plot_to  : new Date(end_time * 1000).getTime(),
                width    : data.width ? data.width : undefined,
            }, options);
        });
    };

    const setXIndicators = () => {
        const exit_tick_index = number_of_ticks - 1;
        if (contract_category.match('asian')) {
            ticks_needed = number_of_ticks;
            x_indicators = {
                _0: { label: 'Entry Spot', id: 'start_tick' },
            };
            x_indicators[`_${exit_tick_index}`] = {
                label: 'Exit Spot',
                id   : 'exit_tick',
            };
        } else if (contract_category.match('callput') || contract_category.match('touchnotouch')) {
            ticks_needed = number_of_ticks + 1;
            x_indicators = {
                _0: { label: 'Entry Spot', id: 'entry_tick' },
            };
            x_indicators[`_${number_of_ticks}`] = {
                label: 'Exit Spot',
                id   : 'exit_tick',
            };
        } else if (contract_category.match('digits')) {
            ticks_needed = number_of_ticks;
            x_indicators = {
                _0: { label: 'Tick 1', id: 'start_tick' },
            };
            x_indicators[`_${exit_tick_index}`] = {
                label: `Tick ${number_of_ticks}`,
                id   : 'last_tick',
            };
        } else {
            x_indicators = {};
        }
    };

    const initializeChart = (config, data) => {
        chart = new Highcharts.Chart({
            chart: {
                type           : 'line',
                renderTo       : 'tick_chart',
                width          : config.width || (config.minimize ? 394 : null),
                height         : config.minimize ? 143 : null,
                backgroundColor: null,
                events         : { load: plot(config.plot_from, config.plot_to) },
                marginLeft     : 50,
            },
            credits: { enabled: false },
            tooltip: {
                formatter() {
                    const new_y = this.y.toFixed(display_decimals);
                    const mom   = moment.utc(applicable_ticks[this.x].epoch * 1000).format('dddd, MMM D, HH:mm:ss');
                    return `${mom}<br/>${display_symbol} ${new_y}`;
                },
            },
            xAxis: {
                type  : 'linear',
                min   : 0,
                max   : number_of_ticks + 1,
                labels: { enabled: false },
            },
            yAxis: {
                opposite: false,
                labels  : {
                    align: 'left',
                    x    : 0,
                },
                title: '',
            },
            series: [{
                data: [],
            }],
            title    : '',
            exporting: { enabled: false, enableImages: false },
            legend   : { enabled: false },
        });
        Highcharts.setOptions({
            lang: { thousandsSep: ',' },
        });
        if (data) {
            dispatch(data);
        }
    };

    const applyChartBackgroundColor = (tick, has_touched) => {
        if (!show_contract_result) return;

        const chart_container = $('#tick_chart');

        const winning = contract_sentiment === 'up' && tick.quote > contract_barrier
            || contract_sentiment === 'down' && tick.quote < contract_barrier
            || contract_sentiment === 'touch' && hasTouched()
            || contract_sentiment === 'notouch' && !hasTouched();

        chart_container.css('background-color', winning
            ? 'rgba(46,136,54,0.198039)'
            : 'rgba(204,0,0,0.098039)');
    };

    const addBarrier = () => {
        if (!set_barrier) {
            return;
        }

        const barrier_type = contract_category.match('asian') ? 'asian' : 'static';

        if (barrier_type === 'static') {
            const first_quote = applicable_ticks[0].quote;
            let barrier_quote = first_quote;

            if (barrier) {
                let final_barrier = barrier_quote + parseFloat(barrier);
                // sometimes due to rounding issues, result is 1.009999 while it should
                // be 1.01
                final_barrier = Number(`${Math.round(`${final_barrier}e${display_decimals}`)}e-${display_decimals}`);
                barrier_quote = final_barrier;
            } else if (abs_barrier) {
                barrier_quote = parseFloat(abs_barrier);
            }

            chart.yAxis[0].addPlotLine({
                id    : 'tick-barrier',
                value : barrier_quote,
                label : { text: `Barrier (${barrier_quote})`, align: 'center' },
                color : 'green',
                width : 2,
                zIndex: 2,
            });
            contract_barrier = barrier_quote;
            set_barrier      = false;
        }

        if (barrier_type === 'asian') {
            let total = 0;
            for (let i = 0; i < applicable_ticks.length; i++) {
                total += parseFloat(applicable_ticks[i].quote);
            }
            // round calculated barrier
            const calc_barrier = (total / applicable_ticks.length).toFixed(parseInt(display_decimals) + 1);

            chart.yAxis[0].removePlotLine('tick-barrier');
            chart.yAxis[0].addPlotLine({
                id   : 'tick-barrier',
                value: calc_barrier,
                color: 'green',
                label: {
                    text : `Average (${calc_barrier})`,
                    align: 'center',
                },
                width : 2,
                zIndex: 2,
            });
            contract_barrier = calc_barrier;
        }
        if (contract_barrier) {
            CommonFunctions.elementInnerHtml(CommonFunctions.getElementById('contract_purchase_barrier'), `${localize('Barrier')}: ${contract_barrier}`);
        }
    };

    const add = (indicator) => {
        chart.xAxis[0].addPlotLine({
            value : indicator.index,
            id    : indicator.id,
            label : { text: indicator.label, x: /start_tick|entry_tick/.test(indicator.id) ? -15 : 5 },
            color : '#e98024',
            width : 2,
            zIndex: 2,
        });
    };

    const evaluateContractOutcome = () => {
        if (!contract_barrier) {
            return; // can't do anything without barrier
        }

        const exit_tick_index = applicable_ticks.length - 1;
        const exit_spot       = applicable_ticks[exit_tick_index].quote;

        if (contract_sentiment === 'up' && exit_spot > contract_barrier
            || contract_sentiment === 'down' && exit_spot < contract_barrier
            || contract_sentiment === 'touch' && hasTouched()
            || contract_sentiment === 'notouch' && !hasTouched()) {
            win();
        } else {
            lose();
        }
    };

    const win = () => {
        updatePurchaseStatus(payout, price, localize('This contract won'));
    };

    const lose = () => {
        updatePurchaseStatus(0, -price, localize('This contract lost'));
    };

    const plot = () => {
        contract_start_moment = moment(contract_start_ms).utc();
        counter               = 0;
        applicable_ticks      = [];
    };

    const hasTouched = () => {
        return !(
            applicable_ticks.every(tick => tick.quote > contract_barrier)
            || applicable_ticks.every(tick => tick.quote < contract_barrier)
        );
    };

    const dispatch = (data) => {
        const tick_chart = CommonFunctions.getElementById('tick_chart');

        if (!CommonFunctions.isVisible(tick_chart) || !data || (!data.tick && !data.history)) {
            return;
        }

        if (subscribe && data.tick && document.getElementById('sell_content_wrapper')) {
            responseID = data.tick.id;
            ViewPopupUI.storeSubscriptionID(responseID);
        }

        let epoches,
            spots2,
            chart_display_decimals;
        if (document.getElementById('sell_content_wrapper')) {
            if (data.tick) {
                Tick.details(data);
                if (!chart_display_decimals) {
                    chart_display_decimals = data.tick.quote.split('.')[1].length || 2;
                }
            } else if (data.history) {
                if (!chart_display_decimals) {
                    chart_display_decimals = data.history.prices[0].split('.')[1].length || 2;
                }
            }
            if (!tick_init) {
                let category = 'callput';
                if (/asian/i.test(tick_shortcode)) {
                    category = 'asian';
                } else if (/digit/i.test(tick_shortcode)) {
                    category = 'digits';
                }
                initialize({
                    symbol              : tick_underlying,
                    number_of_ticks     : tick_count,
                    contract_category   : category,
                    longcode            : tick_longcode,
                    display_symbol      : tick_display_name,
                    contract_start      : tick_date_start,
                    abs_barrier         : absolute_barrier,
                    display_decimals    : chart_display_decimals,
                    show_contract_result: 0,
                }, data);
                spots_list = {};
                tick_init  = 'initialized';
                return;
            }
        }
        if (data.tick) {
            spots2  = Tick.spots();
            epoches = Object.keys(spots2).sort((a, b) => a - b);
        } else if (data.history) {
            epoches = data.history.times;
        }

        const has_reached_end = applicable_ticks && ticks_needed && applicable_ticks.length >= ticks_needed;

        if (has_reached_end || contract_category === 'touchnotouch' && hasTouched()) {
            evaluateContractOutcome();
            if (responseID) {
                BinarySocket.send({ forget: responseID });
            }
        } else {
            for (let d = 0; d < epoches.length; d++) {
                let tick;
                if (data.tick) {
                    tick = {
                        epoch: parseInt(epoches[d]),
                        quote: parseFloat(spots2[epoches[d]]),
                    };
                } else if (data.history) {
                    tick = {
                        epoch: parseInt(data.history.times[d]),
                        quote: parseFloat(data.history.prices[d]),
                    };
                }

                if (contract_start_moment && tick.epoch > contract_start_moment.unix() && !spots_list[tick.epoch]) {
                    if (!chart || !chart.series) return;
                    chart.series[0].addPoint([counter, tick.quote], true, false);
                    applicable_ticks.push(tick);
                    spots_list[tick.epoch] = tick.quote;
                    const indicator_key    = `_${counter}`;
                    if (typeof x_indicators[indicator_key] !== 'undefined') {
                        x_indicators[indicator_key].index = counter;
                        add(x_indicators[indicator_key]);
                    }

                    addBarrier();
                    applyChartBackgroundColor(tick);
                    counter++;
                }
            }
        }
    };

    const updateChart = (data, contract) => {
        subscribe = 'false';
        if (contract) {
            tick_underlying   = contract.underlying;
            tick_count        = contract.tick_count;
            tick_longcode     = contract.longcode;
            tick_display_name = contract.display_name;
            tick_date_start   = contract.date_start;
            absolute_barrier  = contract.barrier;
            tick_shortcode    = contract.shortcode;
            tick_init         = '';
            const request     = {
                ticks_history: contract.underlying,
                start        : contract.date_start,
                end          : 'latest',
            };
            if (contract.current_spot_time < contract.date_expiry) {
                request.subscribe = 1;
                subscribe         = 'true';
            } else {
                request.end = contract.date_expiry;
            }
            BinarySocket.send(request, { callback: dispatch });
        } else {
            dispatch(data);
        }
    };

    return {
        updateChart,
        init      : initialize,
        resetSpots: () => { spots_list = {}; },
    };
})();

module.exports = TickDisplay;
<commit_msg>cleanup<commit_after>const moment               = require('moment');
const Tick                 = require('./tick');
const updatePurchaseStatus = require('./update_values').updatePurchaseStatus;
const ViewPopupUI          = require('../user/view_popup/view_popup.ui');
const BinarySocket         = require('../../base/socket');
const CommonFunctions      = require('../../../_common/common_functions');
const localize             = require('../../../_common/localize').localize;

const TickDisplay = (() => {
    let number_of_ticks,
        display_symbol,
        contract_start_ms,
        contract_category,
        set_barrier,
        barrier,
        abs_barrier,
        display_decimals,
        show_contract_result,
        contract_sentiment,
        price,
        payout,
        ticks_needed,
        x_indicators,
        chart,
        Highcharts,
        applicable_ticks,
        contract_barrier,
        contract_start_moment,
        counter,
        spots_list,
        tick_underlying,
        tick_count,
        tick_longcode,
        tick_display_name,
        tick_date_start,
        absolute_barrier,
        tick_shortcode,
        tick_init,
        subscribe,
        responseID;

    const initialize = (data, options) => {
        // setting up globals
        number_of_ticks      = parseInt(data.number_of_ticks);
        display_symbol       = data.display_symbol;
        contract_start_ms    = parseInt(data.contract_start) * 1000;
        contract_category    = data.contract_category;
        set_barrier          = !contract_category.match('digits');
        barrier              = data.barrier;
        abs_barrier          = data.abs_barrier;
        display_decimals     = data.display_decimals || 2;
        show_contract_result = data.show_contract_result;

        if (data.show_contract_result) {
            contract_sentiment = data.contract_sentiment;
            price              = parseFloat(data.price);
            payout             = parseFloat(data.payout);
        }

        const minimize = data.show_contract_result;
        const end_time = parseInt(data.contract_start) + parseInt((number_of_ticks + 2) * 5);

        setXIndicators();
        CommonFunctions.requireHighstock((Highstock) => {
            Highcharts = Highstock;
            initializeChart({
                minimize,
                plot_from: data.previous_tick_epoch * 1000,
                plot_to  : new Date(end_time * 1000).getTime(),
                width    : data.width ? data.width : undefined,
            }, options);
        });
    };

    const setXIndicators = () => {
        const exit_tick_index = number_of_ticks - 1;
        if (contract_category.match('asian')) {
            ticks_needed = number_of_ticks;
            x_indicators = {
                _0: { label: 'Entry Spot', id: 'start_tick' },
            };
            x_indicators[`_${exit_tick_index}`] = {
                label: 'Exit Spot',
                id   : 'exit_tick',
            };
        } else if (contract_category.match('callput') || contract_category.match('touchnotouch')) {
            ticks_needed = number_of_ticks + 1;
            x_indicators = {
                _0: { label: 'Entry Spot', id: 'entry_tick' },
            };
            x_indicators[`_${number_of_ticks}`] = {
                label: 'Exit Spot',
                id   : 'exit_tick',
            };
        } else if (contract_category.match('digits')) {
            ticks_needed = number_of_ticks;
            x_indicators = {
                _0: { label: 'Tick 1', id: 'start_tick' },
            };
            x_indicators[`_${exit_tick_index}`] = {
                label: `Tick ${number_of_ticks}`,
                id   : 'last_tick',
            };
        } else {
            x_indicators = {};
        }
    };

    const initializeChart = (config, data) => {
        chart = new Highcharts.Chart({
            chart: {
                type           : 'line',
                renderTo       : 'tick_chart',
                width          : config.width || (config.minimize ? 394 : null),
                height         : config.minimize ? 143 : null,
                backgroundColor: null,
                events         : { load: plot(config.plot_from, config.plot_to) },
                marginLeft     : 50,
            },
            credits: { enabled: false },
            tooltip: {
                formatter() {
                    const new_y = this.y.toFixed(display_decimals);
                    const mom   = moment.utc(applicable_ticks[this.x].epoch * 1000).format('dddd, MMM D, HH:mm:ss');
                    return `${mom}<br/>${display_symbol} ${new_y}`;
                },
            },
            xAxis: {
                type  : 'linear',
                min   : 0,
                max   : number_of_ticks + 1,
                labels: { enabled: false },
            },
            yAxis: {
                opposite: false,
                labels  : {
                    align: 'left',
                    x    : 0,
                },
                title: '',
            },
            series: [{
                data: [],
            }],
            title    : '',
            exporting: { enabled: false, enableImages: false },
            legend   : { enabled: false },
        });
        Highcharts.setOptions({
            lang: { thousandsSep: ',' },
        });
        if (data) {
            dispatch(data);
        }
    };

    const applyChartBackgroundColor = (tick) => {
        if (!show_contract_result) return;

        const chart_container = $('#tick_chart');

        const winning = contract_sentiment === 'up' && tick.quote > contract_barrier
            || contract_sentiment === 'down' && tick.quote < contract_barrier
            || contract_sentiment === 'touch' && hasTouched()
            || contract_sentiment === 'notouch' && !hasTouched();

        chart_container.css('background-color', winning
            ? 'rgba(46,136,54,0.198039)'
            : 'rgba(204,0,0,0.098039)');
    };

    const addBarrier = () => {
        if (!set_barrier) {
            return;
        }

        const barrier_type = contract_category.match('asian') ? 'asian' : 'static';

        if (barrier_type === 'static') {
            const first_quote = applicable_ticks[0].quote;
            let barrier_quote = first_quote;

            if (barrier) {
                let final_barrier = barrier_quote + parseFloat(barrier);
                // sometimes due to rounding issues, result is 1.009999 while it should
                // be 1.01
                final_barrier = Number(`${Math.round(`${final_barrier}e${display_decimals}`)}e-${display_decimals}`);
                barrier_quote = final_barrier;
            } else if (abs_barrier) {
                barrier_quote = parseFloat(abs_barrier);
            }

            chart.yAxis[0].addPlotLine({
                id    : 'tick-barrier',
                value : barrier_quote,
                label : { text: `Barrier (${barrier_quote})`, align: 'center' },
                color : 'green',
                width : 2,
                zIndex: 2,
            });
            contract_barrier = barrier_quote;
            set_barrier      = false;
        }

        if (barrier_type === 'asian') {
            let total = 0;
            for (let i = 0; i < applicable_ticks.length; i++) {
                total += parseFloat(applicable_ticks[i].quote);
            }
            // round calculated barrier
            const calc_barrier = (total / applicable_ticks.length).toFixed(parseInt(display_decimals) + 1);

            chart.yAxis[0].removePlotLine('tick-barrier');
            chart.yAxis[0].addPlotLine({
                id   : 'tick-barrier',
                value: calc_barrier,
                color: 'green',
                label: {
                    text : `Average (${calc_barrier})`,
                    align: 'center',
                },
                width : 2,
                zIndex: 2,
            });
            contract_barrier = calc_barrier;
        }
        if (contract_barrier) {
            CommonFunctions.elementInnerHtml(CommonFunctions.getElementById('contract_purchase_barrier'), `${localize('Barrier')}: ${contract_barrier}`);
        }
    };

    const add = (indicator) => {
        chart.xAxis[0].addPlotLine({
            value : indicator.index,
            id    : indicator.id,
            label : { text: indicator.label, x: /start_tick|entry_tick/.test(indicator.id) ? -15 : 5 },
            color : '#e98024',
            width : 2,
            zIndex: 2,
        });
    };

    const evaluateContractOutcome = () => {
        if (!contract_barrier) {
            return; // can't do anything without barrier
        }

        const exit_tick_index = applicable_ticks.length - 1;
        const exit_spot       = applicable_ticks[exit_tick_index].quote;

        if (contract_sentiment === 'up' && exit_spot > contract_barrier
            || contract_sentiment === 'down' && exit_spot < contract_barrier
            || contract_sentiment === 'touch' && hasTouched()
            || contract_sentiment === 'notouch' && !hasTouched()) {
            win();
        } else {
            lose();
        }
    };

    const win = () => {
        updatePurchaseStatus(payout, price, localize('This contract won'));
    };

    const lose = () => {
        updatePurchaseStatus(0, -price, localize('This contract lost'));
    };

    const plot = () => {
        contract_start_moment = moment(contract_start_ms).utc();
        counter               = 0;
        applicable_ticks      = [];
    };

    const hasTouched = () => {
        return !(
            applicable_ticks.every(tick => tick.quote > contract_barrier)
            || applicable_ticks.every(tick => tick.quote < contract_barrier)
        );
    };

    const dispatch = (data) => {
        const tick_chart = CommonFunctions.getElementById('tick_chart');

        if (!CommonFunctions.isVisible(tick_chart) || !data || (!data.tick && !data.history)) {
            return;
        }

        if (subscribe && data.tick && document.getElementById('sell_content_wrapper')) {
            responseID = data.tick.id;
            ViewPopupUI.storeSubscriptionID(responseID);
        }

        let epoches,
            spots2,
            chart_display_decimals;
        if (document.getElementById('sell_content_wrapper')) {
            if (data.tick) {
                Tick.details(data);
                if (!chart_display_decimals) {
                    chart_display_decimals = data.tick.quote.split('.')[1].length || 2;
                }
            } else if (data.history) {
                if (!chart_display_decimals) {
                    chart_display_decimals = data.history.prices[0].split('.')[1].length || 2;
                }
            }
            if (!tick_init) {
                let category = 'callput';
                if (/asian/i.test(tick_shortcode)) {
                    category = 'asian';
                } else if (/digit/i.test(tick_shortcode)) {
                    category = 'digits';
                }
                initialize({
                    symbol              : tick_underlying,
                    number_of_ticks     : tick_count,
                    contract_category   : category,
                    longcode            : tick_longcode,
                    display_symbol      : tick_display_name,
                    contract_start      : tick_date_start,
                    abs_barrier         : absolute_barrier,
                    display_decimals    : chart_display_decimals,
                    show_contract_result: 0,
                }, data);
                spots_list = {};
                tick_init  = 'initialized';
                return;
            }
        }
        if (data.tick) {
            spots2  = Tick.spots();
            epoches = Object.keys(spots2).sort((a, b) => a - b);
        } else if (data.history) {
            epoches = data.history.times;
        }

        const has_reached_end = applicable_ticks && ticks_needed && applicable_ticks.length >= ticks_needed;

        if (has_reached_end || contract_category === 'touchnotouch' && hasTouched()) {
            evaluateContractOutcome();
            if (responseID) {
                BinarySocket.send({ forget: responseID });
            }
        } else {
            for (let d = 0; d < epoches.length; d++) {
                let tick;
                if (data.tick) {
                    tick = {
                        epoch: parseInt(epoches[d]),
                        quote: parseFloat(spots2[epoches[d]]),
                    };
                } else if (data.history) {
                    tick = {
                        epoch: parseInt(data.history.times[d]),
                        quote: parseFloat(data.history.prices[d]),
                    };
                }

                if (contract_start_moment && tick.epoch > contract_start_moment.unix() && !spots_list[tick.epoch]) {
                    if (!chart || !chart.series) return;
                    chart.series[0].addPoint([counter, tick.quote], true, false);
                    applicable_ticks.push(tick);
                    spots_list[tick.epoch] = tick.quote;
                    const indicator_key    = `_${counter}`;
                    if (typeof x_indicators[indicator_key] !== 'undefined') {
                        x_indicators[indicator_key].index = counter;
                        add(x_indicators[indicator_key]);
                    }

                    addBarrier();
                    applyChartBackgroundColor(tick);
                    counter++;
                }
            }
        }
    };

    const updateChart = (data, contract) => {
        subscribe = 'false';
        if (contract) {
            tick_underlying   = contract.underlying;
            tick_count        = contract.tick_count;
            tick_longcode     = contract.longcode;
            tick_display_name = contract.display_name;
            tick_date_start   = contract.date_start;
            absolute_barrier  = contract.barrier;
            tick_shortcode    = contract.shortcode;
            tick_init         = '';
            const request     = {
                ticks_history: contract.underlying,
                start        : contract.date_start,
                end          : 'latest',
            };
            if (contract.current_spot_time < contract.date_expiry) {
                request.subscribe = 1;
                subscribe         = 'true';
            } else {
                request.end = contract.date_expiry;
            }
            BinarySocket.send(request, { callback: dispatch });
        } else {
            dispatch(data);
        }
    };

    return {
        updateChart,
        init      : initialize,
        resetSpots: () => { spots_list = {}; },
    };
})();

module.exports = TickDisplay;
<|endoftext|>


<commit_before>import { Map, fromJS } from 'immutable';
import { expect } from 'chai';
import sinon from 'sinon';

import * as actions from '../../actions';
import * as comms from '../../utils/comms';
import notificationsMiddleware from '../../middleware/notifications';
import NativeNotifications from '../../utils/notifications';
import Helpers from '../../utils/helpers';

const newNotification = [
  {
    id: 987654,
    repo: {
      id: 13,
      full_name: 'manosim/gitify'
    }
  }
];

const notificationsList = fromJS([
  {
    id: 123,
    repository: {
      id: 111111,
      full_name: 'manosim/gitify'
    }
  },
  {
    id: 456,
    repository: {
      id: 111111,
      full_name: 'manosim/gitify'
    }
  },
  {
    id: 789,
    repository: {
      id: 222222,
      full_name: 'facebook/react'
    }
  }
]);

const createFakeStore = fakeData => ({
  getState() {
    return {
      notifications: Map({
        response: notificationsList
      }),
      settings: Map({
        playSound: false,
        showNotifications: false
      })
    };
  }
});

const dispatchWithStoreOf = (storeData, action) => {
  let dispatched = null;
  const dispatch = notificationsMiddleware(createFakeStore(storeData))(actionAttempt => dispatched = actionAttempt);
  dispatch(action);
  return dispatched;
};

describe('middleware/notifications.js', () => {

  beforeEach(function() {

  });

  it('should raise notifications (native & sound)', () => {

    sinon.spy(NativeNotifications, 'setup');

    const action = {
      type: actions.NOTIFICATIONS.SUCCESS,
      payload: newNotification
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);
    expect(NativeNotifications.setup).to.have.been.calledOnce;
    expect(NativeNotifications.setup).to.have.been.calledWith(
      newNotification, Map({ playSound: false, showNotifications: false })
    );

    NativeNotifications.setup.restore();

  });

  it('should mark a notification and call the update tray icon helper', () => {

    sinon.spy(comms, 'updateTrayIcon');

    const action = {
      type: actions.MARK_NOTIFICATION.SUCCESS,
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);

    expect(comms.updateTrayIcon).to.have.been.calledOnce;
    expect(comms.updateTrayIcon).to.have.been.calledWith(2);

    comms.updateTrayIcon.restore();

  });

  it('should mark a repo\'s notification and call the update tray icon helper', () => {

    sinon.spy(comms, 'updateTrayIcon');

    const action = {
      type: actions.MARK_REPO_NOTIFICATION.SUCCESS,
      meta: {
        repoSlug: 'manosim/gitify'
      }
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);
    expect(comms.updateTrayIcon).to.have.been.calledOnce;
    expect(comms.updateTrayIcon).to.have.been.calledWith(1);

    comms.updateTrayIcon.restore();

  });

});
<commit_msg>Linting<commit_after>import { Map, fromJS } from 'immutable';
import { expect } from 'chai';
import sinon from 'sinon';

import * as actions from '../../actions';
import * as comms from '../../utils/comms';
import notificationsMiddleware from '../../middleware/notifications';
import NativeNotifications from '../../utils/notifications';

const newNotification = [
  {
    id: 987654,
    repo: {
      id: 13,
      full_name: 'manosim/gitify'
    }
  }
];

const notificationsList = fromJS([
  {
    id: 123,
    repository: {
      id: 111111,
      full_name: 'manosim/gitify'
    }
  },
  {
    id: 456,
    repository: {
      id: 111111,
      full_name: 'manosim/gitify'
    }
  },
  {
    id: 789,
    repository: {
      id: 222222,
      full_name: 'facebook/react'
    }
  }
]);

const createFakeStore = fakeData => ({
  getState() {
    return {
      notifications: Map({
        response: notificationsList
      }),
      settings: Map({
        playSound: false,
        showNotifications: false
      })
    };
  }
});

const dispatchWithStoreOf = (storeData, action) => {
  let dispatched = null;
  const dispatch = notificationsMiddleware(createFakeStore(storeData))(actionAttempt => dispatched = actionAttempt);
  dispatch(action);
  return dispatched;
};

describe('middleware/notifications.js', () => {

  beforeEach(function() {

  });

  it('should raise notifications (native & sound)', () => {

    sinon.spy(NativeNotifications, 'setup');

    const action = {
      type: actions.NOTIFICATIONS.SUCCESS,
      payload: newNotification
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);
    expect(NativeNotifications.setup).to.have.been.calledOnce;
    expect(NativeNotifications.setup).to.have.been.calledWith(
      newNotification, Map({ playSound: false, showNotifications: false })
    );

    NativeNotifications.setup.restore();

  });

  it('should mark a notification and call the update tray icon helper', () => {

    sinon.spy(comms, 'updateTrayIcon');

    const action = {
      type: actions.MARK_NOTIFICATION.SUCCESS,
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);

    expect(comms.updateTrayIcon).to.have.been.calledOnce;
    expect(comms.updateTrayIcon).to.have.been.calledWith(2);

    comms.updateTrayIcon.restore();

  });

  it('should mark a repo\'s notification and call the update tray icon helper', () => {

    sinon.spy(comms, 'updateTrayIcon');

    const action = {
      type: actions.MARK_REPO_NOTIFICATION.SUCCESS,
      meta: {
        repoSlug: 'manosim/gitify'
      }
    };

    expect(dispatchWithStoreOf({}, action)).to.eql(action);
    expect(comms.updateTrayIcon).to.have.been.calledOnce;
    expect(comms.updateTrayIcon).to.have.been.calledWith(1);

    comms.updateTrayIcon.restore();

  });

});
<|endoftext|>


<commit_before>/**
* This file is a version of https://github.com/steveukx/hogan-middleware adapted for our requirements.
*/
var Hogan = require('hogan.js');
var ReadDir = require('readdir');
var Path = require('path');
var FS = require('fs');
var argv = require('minimist')(process.argv.slice(2));

function TemplateEngine() {
}

/**
 * All active directory file system watches
 * @type {fs.FSWatcher[]}
 * @ignore
 */
TemplateEngine._watches = [];

/**
 * Called by the express server to get the content for a given template at the templatePath supplied. The templateData
 * can contain any content from a configured route, and will be made available to the templates.
 *
 * Templates can include partials by name for any template also in the views directory, note that if sub-directories are
 * used to create included partials, express will not necessarily recognise that file as a valid view path... you've been
 * warned.
 *
 * @param {String} templatePath Path to the template
 * @param {Object} templateData Data to give to the template
 * @param {Function} next Callback to receive two arguments, an error object and the template result.
 */
TemplateEngine.__express = function (templatePath, templateData, next) {
   var templatePaths = [templateData.settings.views, templateData.settings.vendorViews];
   var templateName;

   for (var i = 0, j = templatePaths.length; i < j; i++) {
      if (templatePath.indexOf(templatePaths[i]) === 0) {
         templateName = TemplateEngine._getTemplateName(templatePaths[i], templatePath);

         break;
      }
   }

   if (!templateName) {
      templateName = Path.basename(templatePath, Path.extname(templatePath));
   }

   var templates = TemplateEngine._getTemplates(templatePaths);
   var output = null, error = null;

   try {
      output = templates[templateName].render(templateData, templates);
   }
   catch (e) {
      error = e;
   }
   finally {
      next(error, output);
   }
};

/**
 * Remove the base path from a template path, and the extension to generate the template name
 *
 * @param {String} basePath
 * @param {String} templatePath
 * @returns {String}
 * @private
 */
TemplateEngine._getTemplateName = function (basePath, templatePath) {
   var relativePath = Path.relative(basePath, templatePath);
   var templateName = Path.join(Path.dirname(relativePath), Path.basename(templatePath, Path.extname(templatePath)));

   return templateName;
};

/**

 *
 * @param {function} templatePath
 */

/**
 * Return a function that stores an individual template based on the supplied path, the name of the template is the
 * file's relative path without the extension.
 *
 * @param basePath
 * @returns {Function}
 * @private
 */
TemplateEngine._storeTemplate = function (basePath) {
   return function (templatePath) {
      var templateName = TemplateEngine._getTemplateName(basePath, templatePath);

      TemplateEngine.__templates[templateName] = Hogan.compile(FS.readFileSync(templatePath, 'utf-8'));

      if (argv.verbose){
         console.log('Stored template', templateName);
      }
   };
};

/**
 * Gets all templates, when the template path hasn't yet been scanned it will be read synchronously to ensure there are
 * always templates available.
 *
 * @param {Array} templatePaths
 */
TemplateEngine._getTemplates = function (templatePaths) {
   TemplateEngine.__templates = {};
   for (var i = 0, j = templatePaths.length; i < j; i++) {
      ReadDir.readSync(templatePaths[i], ['**.html'], ReadDir.ABSOLUTE_PATHS)
         .forEach(TemplateEngine._storeTemplate(templatePaths[i]), TemplateEngine);
   }
   return TemplateEngine.__templates;
};

module.exports = TemplateEngine;
<commit_msg>PP-157 Allow template engine to have multiple source diretories. This makes sample-integration slightly easier to extend.<commit_after>/**
* This file is a version of https://github.com/steveukx/hogan-middleware adapted for our requirements.
*/
var Hogan = require('hogan.js');
var ReadDir = require('readdir');
var Path = require('path');
var FS = require('fs');
var argv = require('minimist')(process.argv.slice(2));

function TemplateEngine() {
}

/**
 * All active directory file system watches
 * @type {fs.FSWatcher[]}
 * @ignore
 */
TemplateEngine._watches = [];

/**
 * Called by the express server to get the content for a given template at the templatePath supplied. The templateData
 * can contain any content from a configured route, and will be made available to the templates.
 *
 * Templates can include partials by name for any template also in the views directory, note that if sub-directories are
 * used to create included partials, express will not necessarily recognise that file as a valid view path... you've been
 * warned.
 *
 * @param {String} templatePath Path to the template
 * @param {Object} templateData Data to give to the template
 * @param {Function} next Callback to receive two arguments, an error object and the template result.
 */
TemplateEngine.__express = function (templatePath, templateData, next) {
   var templatePaths = [].concat.apply([], [templateData.settings.views, templateData.settings.vendorViews]);
   var templateName;

   for (var i = 0, j = templatePaths.length; i < j; i++) {
      if (templatePath.indexOf(templatePaths[i]) === 0) {
         templateName = TemplateEngine._getTemplateName(templatePaths[i], templatePath);
         break;
      }
   }

   if (!templateName) {
      templateName = Path.basename(templatePath, Path.extname(templatePath));
   }

   var templates = TemplateEngine._getTemplates(templatePaths);
   var output = null, error = null;

   try {
      output = templates[templateName].render(templateData, templates);
   }
   catch (e) {
      error = e;
   }
   finally {
      next(error, output);
   }
};

/**
 * Remove the base path from a template path, and the extension to generate the template name
 *
 * @param {String} basePath
 * @param {String} templatePath
 * @returns {String}
 * @private
 */
TemplateEngine._getTemplateName = function (basePath, templatePath) {
   var relativePath = Path.relative(basePath, templatePath);
   var templateName = Path.join(Path.dirname(relativePath), Path.basename(templatePath, Path.extname(templatePath)));

   return templateName;
};

/**

 *
 * @param {function} templatePath
 */

/**
 * Return a function that stores an individual template based on the supplied path, the name of the template is the
 * file's relative path without the extension.
 *
 * @param basePath
 * @returns {Function}
 * @private
 */
TemplateEngine._storeTemplate = function (basePath) {
   return function (templatePath) {
      var templateName = TemplateEngine._getTemplateName(basePath, templatePath);

      TemplateEngine.__templates[templateName] = Hogan.compile(FS.readFileSync(templatePath, 'utf-8'));

      if (argv.verbose){
         console.log('Stored template', templateName);
      }
   };
};

/**
 * Gets all templates, when the template path hasn't yet been scanned it will be read synchronously to ensure there are
 * always templates available.
 *
 * @param {Array} templatePaths
 */
TemplateEngine._getTemplates = function (templatePaths) {
   TemplateEngine.__templates = {};
   for (var i = 0, j = templatePaths.length; i < j; i++) {
      ReadDir.readSync(templatePaths[i], ['**.html'], ReadDir.ABSOLUTE_PATHS)
         .forEach(TemplateEngine._storeTemplate(templatePaths[i]), TemplateEngine);
   }
   return TemplateEngine.__templates;
};

module.exports = TemplateEngine;
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import KeyBinding from 'react-keybinding-component';

import TrackRow from './TrackRow.react';
import PlayingIndicator from './PlayingIndicator.react';

import AppActions from '../../actions/AppActions';

import Player from '../../lib/player';
import utils  from '../../utils/utils';

const ipcRenderer = electron.ipcRenderer;

/*
|--------------------------------------------------------------------------
| Child - ArtistList
|--------------------------------------------------------------------------
*/

export default class TracksList extends Component {

    static propTypes = {
        type: React.PropTypes.string.isRequired,
        tracks: React.PropTypes.array,
        trackPlayingId: React.PropTypes.string,
        playlists: React.PropTypes.array,
        currentPlaylist: React.PropTypes.string
    }

    constructor(props) {

        super(props);
        this.state = {
            selected  : [],
            scrollTop : 0
        };

        this.showContextMenu  = this.showContextMenu.bind(this);
        this.scrollTracksList = this.scrollTracksList.bind(this);
        this.selectTrack      = this.selectTrack.bind(this);
        this.onKey            = this.onKey.bind(this);

        this.rowHeight = 30;
    }

    render() {
        const tracks = [...this.props.tracks];

        // TODO (y.solovyov | KeitIG): TrackListHeader component?
        return (

                <KeyBinding onKey={ this.onKey } target={ '.tracks-list-container' } preventInputConflict preventDefault />

Track

Duration

Artist

Album

Genre

                        { this._buildTrackTiles() }

        );
    }

    componentDidMount() {

        const self = this;

        ipcRenderer.on('tracksListContextMenuReply', (event, reply, data) => {

            const selected = self.state.selected;

            switch(reply) {
                case 'addToQueue': {
                    AppActions.queue.add(selected);
                    break;
                }
                case 'playNext': {
                    AppActions.queue.addNext(selected);
                    break;
                }
                case 'addToPlaylist': {
                    const isShown = self.props.type === 'playlist' && data === self.props.currentPlaylist;
                    AppActions.playlists.addTracksTo(data.playlistId, selected, isShown);
                    break;
                }
                case 'removeFromPlaylist': {
                    if(self.props.type === 'playlist') {
                        AppActions.playlists.removeTracksFrom(self.props.currentPlaylist, selected);
                    }
                    break;
                }
                case 'createPlaylist': {
                    AppActions.playlists.create('New playlist', false, (playlistId) => {
                        const isShown = self.props.type === 'playlist' && data === self.props.currentPlaylist;
                        AppActions.playlists.addTracksTo(playlistId, selected, isShown);
                    });
                    break;
                }
                case 'searchFor': {
                    // small hack, we can't call AppActions.library.filterSearch directly
                    // otherwise the search clear button will not appear, because it will not detect an input event on itself
                    const searchInput = document.querySelector('input[type="text"].search');
                    searchInput.value = data.search;
                    searchInput.dispatchEvent(new Event('input', { bubbles: true }));
                    break;
                }
            }
        });
    }

    componentWillUnmount() {
        ipcRenderer.removeAllListeners('tracksListContextMenuReply');
    }

    scrollTracksList() {
        this.setState({ scrollTop : document.querySelector('.tracks-list-body').scrollTop });
    }

    selectTrack(e, id, index) {

        const self   = this;
        const tracks = this.props.tracks;

        if(e.button === 0 || (e.button === 2 && !this.state.selected.includes(id))) {
            if(e.ctrlKey) { // add/remove one track in selected tracks

                this._toggleSelectionById(id);

            } else if (e.shiftKey) { // add multiple tracks in selected tracks

                const selected = this.state.selected;

                switch(selected.length) {
                    case 0: {
                        selected.push(id);
                        this.setState({ selected });
                        break;
                    }
                    case 1: {

                        const onlySelected = selected[0];
                        let onlySelectedIndex;

                        for(let i = 0, length = tracks.length; i < length; i++) { // STH wrong here
                            if(tracks[i]._id === onlySelected) {
                                onlySelectedIndex = i;
                                break;
                            }
                        }

                        if(index < onlySelectedIndex) {
                            for(let i = 1; i <= Math.abs(index - onlySelectedIndex); i++) {
                                selected.push(tracks[onlySelectedIndex - i]._id);
                            }
                        } else if(index > onlySelectedIndex) {
                            for(let i = 1; i <= Math.abs(index - onlySelectedIndex); i++) {
                                selected.push(tracks[onlySelectedIndex + i]._id);
                            }
                        }

                        self.setState({ selected });
                        break;
                    }
                    default: {
                        const selectedInt = [];

                        for(let i = 0, length = tracks.length; i < length; i++) {
                            if(selected.includes(tracks[i]._id)) {
                                selectedInt.push(i);
                            }
                        }

                        let base;
                        const min = Math.min(...selectedInt);
                        const max = Math.max(...selectedInt);

                        if(index < min) {
                            base = max;
                        } else {
                            base = min;
                        }

                        const newSelected = [];

                        if(index < min) {
                            for(let i = 0; i <= Math.abs(index - base); i++) {
                                newSelected.push(tracks[base - i]._id);
                            }
                        } else if(index > max) {
                            for(let i = 0; i <= Math.abs(index - base); i++) {
                                newSelected.push(tracks[base + i]._id);
                            }
                        }

                        self.setState({ selected : newSelected });
                        break;
                    }
                }
            } else { // simple select
                const selected = [id];
                this.setState({ selected });
            }
        }
    }

    onKey(e) {
        const selected = this.state.selected,
            tracks     = this.props.tracks;

        const firstSelectedTrackIdx = tracks.findIndex((track) => selected.includes(track._id));

        switch(e.keyCode) {
            case 38: // up
                this._onUp(firstSelectedTrackIdx, tracks);
                break;

            case 40: // down
                this._onDown(firstSelectedTrackIdx, tracks);
                break;

            case 13: // enter
                this._onEnter(firstSelectedTrackIdx, tracks);
                break;
        }
    }

    _buildTrackTiles() {
        const self         = this,
            selected       = this.state.selected,
            tracks         = [...this.props.tracks],
            trackPlayingId = this.props.trackPlayingId;

        const chunkLength = 20;
        const tilesToDisplay = 5;
        const tileHeight = this.rowHeight * chunkLength;

        const tracksChunked = utils.chunkArray(tracks, chunkLength);
        const tilesScrolled = Math.floor(this.state.scrollTop / tileHeight);

        return tracksChunked.splice(tilesScrolled, tilesToDisplay).map((tracksChunk, indexChunk) => {

            const list = tracksChunk.map((track, index) => {

                let playingIndicator = undefined;

                if(trackPlayingId === track._id) {
                    playingIndicator = <PlayingIndicator state={ Player.getAudio().paused ? 'pause' : 'play' } />;
                }

                return(
                    <TrackRow
                        selected={ selected.includes(track._id) }
                        trackId={ track._id }
                        key={ index }
                        index={ (tilesScrolled + indexChunk) * chunkLength + index }
                        onMouseDown={ self.selectTrack }
                        onContextMenu={ self.showContextMenu }
                    >

                            { playingIndicator }

                            { track.title }

                            { utils.parseDuration(track.duration) }

                            { track.artist[0] }

                            { track.album }

                            { track.genre.join(', ') }

                    </TrackRow>
                );
            });

            const tracksListTileStyles = {
                transform: `translate3d(0, ${(((tilesScrolled * this.rowHeight * chunkLength) + (indexChunk * this.rowHeight * chunkLength)))}px, 0)`
            };

            return (

                    { list }

            );
        });
    }

    _toggleSelectionById(id) {
        let selected = [...this.state.selected];

        if(selected.includes(id)) {
            // remove track
            selected.splice(selected.indexOf(id), 1);

        } else {
            // add track
            selected.push(id);
        }

        selected = utils.simpleSort(selected, 'asc');
        this.setState({ selected });
    }

    _onUp(i, tracks) {
        if(i - 1 >= 0) {
            this.setState({ selected : tracks[i - 1]._id }, () => {

                const container = document.querySelector('.tracks-list-container .tracks-list-body');
                const nodeOffsetTop = (i - 1) * this.rowHeight;

                if(container.scrollTop > nodeOffsetTop) container.scrollTop = nodeOffsetTop;
            });
        }
    }

    _onDown(i, tracks) {
        if(i + 1 < tracks.length) {
            this.setState({ selected : tracks[i + 1]._id }, () => {

                const container = document.querySelector('.tracks-list-container .tracks-list-body');
                const nodeOffsetTop = (i + 1) * this.rowHeight;

                if(container.scrollTop + container.offsetHeight <= nodeOffsetTop) {
                    container.scrollTop = nodeOffsetTop - container.offsetHeight + this.rowHeight;
                }
            });
        }
    }

    _onEnter(i, tracks) {
        if(i !== undefined) AppActions.library.selectAndPlay(tracks[i]._id);
    }

    showContextMenu(e, index) {

        let playlistsList = [].concat(this.props.playlists);

        // Hide current playlist if needed
        if(this.props.type === 'playlist') {
            playlistsList = playlistsList.filter((elem) => elem._id !== this.props.currentPlaylist);
        }

        ipcRenderer.send('tracksListContextMenu', JSON.stringify({
            type: this.props.type,
            selectedCount: this.state.selected.length,
            track: this.props.tracks[index],
            playlists: playlistsList
        }));
    }
}
<commit_msg>Clean up TrackList#selectTrack<commit_after>import React, { Component } from 'react';
import KeyBinding from 'react-keybinding-component';

import TrackRow from './TrackRow.react';
import PlayingIndicator from './PlayingIndicator.react';

import AppActions from '../../actions/AppActions';

import Player from '../../lib/player';
import utils  from '../../utils/utils';

const ipcRenderer = electron.ipcRenderer;

/*
|--------------------------------------------------------------------------
| Child - ArtistList
|--------------------------------------------------------------------------
*/

export default class TracksList extends Component {

    static propTypes = {
        type: React.PropTypes.string.isRequired,
        tracks: React.PropTypes.array,
        trackPlayingId: React.PropTypes.string,
        playlists: React.PropTypes.array,
        currentPlaylist: React.PropTypes.string
    }

    constructor(props) {

        super(props);
        this.state = {
            selected  : [],
            scrollTop : 0
        };

        this.showContextMenu  = this.showContextMenu.bind(this);
        this.scrollTracksList = this.scrollTracksList.bind(this);
        this.selectTrack      = this.selectTrack.bind(this);
        this.onKey            = this.onKey.bind(this);

        this.rowHeight = 30;
    }

    render() {
        const tracks = [...this.props.tracks];

        // TODO (y.solovyov | KeitIG): TrackListHeader component?
        return (

                <KeyBinding onKey={ this.onKey } target={ '.tracks-list-container' } preventInputConflict preventDefault />

Track

Duration

Artist

Album

Genre

                        { this._buildTrackTiles() }

        );
    }

    componentDidMount() {

        const self = this;

        ipcRenderer.on('tracksListContextMenuReply', (event, reply, data) => {

            const selected = self.state.selected;

            switch(reply) {
                case 'addToQueue': {
                    AppActions.queue.add(selected);
                    break;
                }
                case 'playNext': {
                    AppActions.queue.addNext(selected);
                    break;
                }
                case 'addToPlaylist': {
                    const isShown = self.props.type === 'playlist' && data === self.props.currentPlaylist;
                    AppActions.playlists.addTracksTo(data.playlistId, selected, isShown);
                    break;
                }
                case 'removeFromPlaylist': {
                    if(self.props.type === 'playlist') {
                        AppActions.playlists.removeTracksFrom(self.props.currentPlaylist, selected);
                    }
                    break;
                }
                case 'createPlaylist': {
                    AppActions.playlists.create('New playlist', false, (playlistId) => {
                        const isShown = self.props.type === 'playlist' && data === self.props.currentPlaylist;
                        AppActions.playlists.addTracksTo(playlistId, selected, isShown);
                    });
                    break;
                }
                case 'searchFor': {
                    // small hack, we can't call AppActions.library.filterSearch directly
                    // otherwise the search clear button will not appear, because it will not detect an input event on itself
                    const searchInput = document.querySelector('input[type="text"].search');
                    searchInput.value = data.search;
                    searchInput.dispatchEvent(new Event('input', { bubbles: true }));
                    break;
                }
            }
        });
    }

    componentWillUnmount() {
        ipcRenderer.removeAllListeners('tracksListContextMenuReply');
    }

    scrollTracksList() {
        this.setState({ scrollTop : document.querySelector('.tracks-list-body').scrollTop });
    }

    selectTrack(e, id, index) {
        if(e.button === 0 || (e.button === 2 && !this.state.selected.includes(id))) {
            if(e.ctrlKey) {
                this._toggleSelectionById(id);
            } else if (e.shiftKey) {
                this._multiSelect(e, id, index);
            } else {
                const selected = [id];
                this.setState({ selected });
            }
        }
    }

    onKey(e) {
        const selected = this.state.selected,
            tracks     = this.props.tracks;

        const firstSelectedTrackIdx = tracks.findIndex((track) => selected.includes(track._id));

        switch(e.keyCode) {
            case 38: // up
                this._onUp(firstSelectedTrackIdx, tracks);
                break;

            case 40: // down
                this._onDown(firstSelectedTrackIdx, tracks);
                break;

            case 13: // enter
                this._onEnter(firstSelectedTrackIdx, tracks);
                break;
        }
    }

    _buildTrackTiles() {
        const self         = this,
            selected       = this.state.selected,
            tracks         = [...this.props.tracks],
            trackPlayingId = this.props.trackPlayingId;

        const chunkLength = 20;
        const tilesToDisplay = 5;
        const tileHeight = this.rowHeight * chunkLength;

        const tracksChunked = utils.chunkArray(tracks, chunkLength);
        const tilesScrolled = Math.floor(this.state.scrollTop / tileHeight);

        return tracksChunked.splice(tilesScrolled, tilesToDisplay).map((tracksChunk, indexChunk) => {

            const list = tracksChunk.map((track, index) => {

                let playingIndicator = undefined;

                if(trackPlayingId === track._id) {
                    playingIndicator = <PlayingIndicator state={ Player.getAudio().paused ? 'pause' : 'play' } />;
                }

                return(
                    <TrackRow
                        selected={ selected.includes(track._id) }
                        trackId={ track._id }
                        key={ index }
                        index={ (tilesScrolled + indexChunk) * chunkLength + index }
                        onMouseDown={ self.selectTrack }
                        onContextMenu={ self.showContextMenu }
                    >

                            { playingIndicator }

                            { track.title }

                            { utils.parseDuration(track.duration) }

                            { track.artist[0] }

                            { track.album }

                            { track.genre.join(', ') }

                    </TrackRow>
                );
            });

            const tracksListTileStyles = {
                transform: `translate3d(0, ${(((tilesScrolled * this.rowHeight * chunkLength) + (indexChunk * this.rowHeight * chunkLength)))}px, 0)`
            };

            return (

                    { list }

            );
        });
    }

    _toggleSelectionById(id) {
        let selected = [...this.state.selected];

        if(selected.includes(id)) {
            // remove track
            selected.splice(selected.indexOf(id), 1);

        } else {
            // add track
            selected.push(id);
        }

        selected = utils.simpleSort(selected, 'asc');
        this.setState({ selected });
    }

    _multiSelect(e, id, index) {
        const self   = this;
        const tracks = this.props.tracks;
        const selected = this.state.selected;

        switch(selected.length) {
            case 0: {
                selected.push(id);
                this.setState({ selected });
                break;
            }
            case 1: {

                const onlySelected = selected[0];
                let onlySelectedIndex;

                for(let i = 0, length = tracks.length; i < length; i++) { // STH wrong here
                    if(tracks[i]._id === onlySelected) {
                        onlySelectedIndex = i;
                        break;
                    }
                }

                if(index < onlySelectedIndex) {
                    for(let i = 1; i <= Math.abs(index - onlySelectedIndex); i++) {
                        selected.push(tracks[onlySelectedIndex - i]._id);
                    }
                } else if(index > onlySelectedIndex) {
                    for(let i = 1; i <= Math.abs(index - onlySelectedIndex); i++) {
                        selected.push(tracks[onlySelectedIndex + i]._id);
                    }
                }

                self.setState({ selected });
                break;
            }
            default: {
                const selectedInt = [];

                for(let i = 0, length = tracks.length; i < length; i++) {
                    if(selected.includes(tracks[i]._id)) {
                        selectedInt.push(i);
                    }
                }

                let base;
                const min = Math.min(...selectedInt);
                const max = Math.max(...selectedInt);

                if(index < min) {
                    base = max;
                } else {
                    base = min;
                }

                const newSelected = [];

                if(index < min) {
                    for(let i = 0; i <= Math.abs(index - base); i++) {
                        newSelected.push(tracks[base - i]._id);
                    }
                } else if(index > max) {
                    for(let i = 0; i <= Math.abs(index - base); i++) {
                        newSelected.push(tracks[base + i]._id);
                    }
                }

                self.setState({ selected : newSelected });
                break;
            }
        }
    }

    _onUp(i, tracks) {
        if(i - 1 >= 0) {
            this.setState({ selected : tracks[i - 1]._id }, () => {

                const container = document.querySelector('.tracks-list-container .tracks-list-body');
                const nodeOffsetTop = (i - 1) * this.rowHeight;

                if(container.scrollTop > nodeOffsetTop) container.scrollTop = nodeOffsetTop;
            });
        }
    }

    _onDown(i, tracks) {
        if(i + 1 < tracks.length) {
            this.setState({ selected : tracks[i + 1]._id }, () => {

                const container = document.querySelector('.tracks-list-container .tracks-list-body');
                const nodeOffsetTop = (i + 1) * this.rowHeight;

                if(container.scrollTop + container.offsetHeight <= nodeOffsetTop) {
                    container.scrollTop = nodeOffsetTop - container.offsetHeight + this.rowHeight;
                }
            });
        }
    }

    _onEnter(i, tracks) {
        if(i !== undefined) AppActions.library.selectAndPlay(tracks[i]._id);
    }

    showContextMenu(e, index) {

        let playlistsList = [].concat(this.props.playlists);

        // Hide current playlist if needed
        if(this.props.type === 'playlist') {
            playlistsList = playlistsList.filter((elem) => elem._id !== this.props.currentPlaylist);
        }

        ipcRenderer.send('tracksListContextMenu', JSON.stringify({
            type: this.props.type,
            selectedCount: this.state.selected.length,
            track: this.props.tracks[index],
            playlists: playlistsList
        }));
    }
}
<|endoftext|>


<commit_before>import icon from 'part:@sanity/base/user-icon'

export default {
  name: 'author',
  type: 'document',
  title: 'Author',
  icon,
  description: 'This represents an author',
  // eslint-disable-next-line camelcase
  __experimental_search: [{path: 'name', weight: 10}],
  preview: {
    select: {
      title: 'name',
      awards: 'awards',
      relatedAuthors: 'relatedAuthors',
      lastUpdated: '_updatedAt',
      media: 'image',
    },
    prepare({title, media, awards}) {
      return {
        title: title,
        media: media,
        subtitle: awards && awards.join(', '),
      }
    },
  },
  fields: [
    {
      name: 'name',
      title: 'Name',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      name: 'bestFriend',
      title: 'Best friend',
      type: 'reference',
      to: [{type: 'author'}],
    },
    {
      name: 'role',
      title: 'Role',
      type: 'string',
      options: {
        list: [
          {value: 'developer', title: 'Developer'},
          {value: 'designer', title: 'Designer'},
          {value: 'ops', title: 'Operations'},
        ],
      },
    },
    {
      name: 'image',
      title: 'Image',
      type: 'image',
      options: {hotspot: true},
    },
    {
      name: 'awards',
      title: 'Awards',
      type: 'array',
      of: [
        {
          type: 'string',
        },
      ],
    },
    {
      name: 'favoriteBooks',
      title: 'Favorite books',
      type: 'array',
      of: [
        {
          type: 'reference',
          to: {type: 'book'},
        },
      ],
    },
    {
      name: 'minimalBlock',
      title: 'Reset all options',
      type: 'array',
      of: [
        {
          type: 'block',
          styles: [],
          lists: [],
          marks: {
            decorators: [],
            annotations: [],
          },
        },
      ],
    },
    {
      name: 'specie',
      title: 'Specie',
      description: 'There are a lot of species. Use this to test reference search performence',
      type: 'reference',
      to: {type: 'species'},
    },
  ],

  initialValue: () => ({
    name: 'Foo',
    bestFriend: {_type: 'reference', _ref: 'foo-bar'},
    image: {
      _type: 'image',
      asset: {
        _ref: 'image-8dcc1391e06e4b4acbdc6bbf2e8c8588d537cbb8-4896x3264-jpg',
        _type: 'reference',
      },
    },
  }),
}
<commit_msg>chore(examples): add example of enum value title lookup in preview prepare<commit_after>import icon from 'part:@sanity/base/user-icon'

const AUTHOR_ROLES = [
  {value: 'developer', title: 'Developer'},
  {value: 'designer', title: 'Designer'},
  {value: 'ops', title: 'Operations'},
]

export default {
  name: 'author',
  type: 'document',
  title: 'Author',
  icon,
  description: 'This represents an author',
  // eslint-disable-next-line camelcase
  __experimental_search: [{path: 'name', weight: 10}],
  preview: {
    select: {
      title: 'name',
      awards: 'awards',
      role: 'role',
      relatedAuthors: 'relatedAuthors',
      lastUpdated: '_updatedAt',
      media: 'image',
    },
    prepare({title, media, awards, role}) {
      const roleName = role ? AUTHOR_ROLES.find((option) => option.value === role) : undefined
      return {
        title: roleName ? `${title} (${roleName})` : title,
        media: media,
        subtitle: awards && awards.join(', '),
      }
    },
  },
  fields: [
    {
      name: 'name',
      title: 'Name',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      name: 'bestFriend',
      title: 'Best friend',
      type: 'reference',
      to: [{type: 'author'}],
    },
    {
      name: 'role',
      title: 'Role',
      type: 'string',
      options: {
        list: AUTHOR_ROLES,
      },
    },
    {
      name: 'image',
      title: 'Image',
      type: 'image',
      options: {hotspot: true},
    },
    {
      name: 'awards',
      title: 'Awards',
      type: 'array',
      of: [
        {
          type: 'string',
        },
      ],
    },
    {
      name: 'favoriteBooks',
      title: 'Favorite books',
      type: 'array',
      of: [
        {
          type: 'reference',
          to: {type: 'book'},
        },
      ],
    },
    {
      name: 'minimalBlock',
      title: 'Reset all options',
      type: 'array',
      of: [
        {
          type: 'block',
          styles: [],
          lists: [],
          marks: {
            decorators: [],
            annotations: [],
          },
        },
      ],
    },
    {
      name: 'specie',
      title: 'Specie',
      description: 'There are a lot of species. Use this to test reference search performence',
      type: 'reference',
      to: {type: 'species'},
    },
  ],

  initialValue: () => ({
    name: 'Foo',
    bestFriend: {_type: 'reference', _ref: 'foo-bar'},
    image: {
      _type: 'image',
      asset: {
        _ref: 'image-8dcc1391e06e4b4acbdc6bbf2e8c8588d537cbb8-4896x3264-jpg',
        _type: 'reference',
      },
    },
  }),
}
<|endoftext|>


<commit_before>// transform
// Functions for transformation matrices, and to be exact,
// affine non-reflective similarity transformation matrices.
//

// ### Constants ###

exports.EPSILON = require('./epsilon')

// Identity transform, a kind of multiplication by 1.
exports.IDENTITY = {
  a: 1,
  b: 0,
  x: 0,
  y: 0
}

// Useful transformations
exports.ROT90 = { a: 0, b: 1, x: 0, y: 0 }
exports.ROT180 = { a: -1, b: 0, x: 0, y: 0 }
exports.ROT270 = { a: 0, b: -1, x: 0, y: 0 }
exports.HALF = { a: 0.5, b: 0, x: 0, y: 0 }
exports.X2 = { a: 2, b: 0, x: 0, y: 0 }

// ### Functions ###

exports.almostEqual =
exports.almostEquals = require('./almostEqual')

exports.create = require('./create')

exports.createFromPolar = require('./createFromPolar')

exports.createFromArray = require('./createFromArray')

exports.equal =
exports.equals = require('./equal')

exports.getRotation = require('./getRotation')

exports.getScale = require('./getScale')

exports.getTranslation = require('./getTranslation')

exports.toArray = require('./toArray')

exports.toMatrix = require('./toMatrix')

exports.toString = require('./toString')

exports.point = require('./point')

exports.points = require('./points')

exports.inverse = require('./inverse')

// exports.addVector = ?
// exports.translateBy =
exports.translate = require('./translate')

exports.scale = require('./scale')

exports.rotate = require('./rotate')

exports.compose =
exports.multiply = require('./multiply')
<commit_msg>New premade transform: ROT45<commit_after>// transform
// Functions for transformation matrices, and to be exact,
// affine non-reflective similarity transformation matrices.
//

// ### Constants ###

exports.EPSILON = require('./epsilon')

// Identity transform, a kind of multiplication by 1.
exports.IDENTITY = {
  a: 1,
  b: 0,
  x: 0,
  y: 0
}

// Useful transformations
const isqrt2 = 1 / Math.sqrt(2)
exports.ROT45 = { a: isqrt2, b: isqrt2, x: 0, y: 0 }
exports.ROT90 = { a: 0, b: 1, x: 0, y: 0 }
exports.ROT180 = { a: -1, b: 0, x: 0, y: 0 }
exports.ROT270 = { a: 0, b: -1, x: 0, y: 0 }
exports.HALF = { a: 0.5, b: 0, x: 0, y: 0 }
exports.X2 = { a: 2, b: 0, x: 0, y: 0 }

// ### Functions ###

exports.almostEqual =
exports.almostEquals = require('./almostEqual')

exports.create = require('./create')

exports.createFromPolar = require('./createFromPolar')

exports.createFromArray = require('./createFromArray')

exports.equal =
exports.equals = require('./equal')

exports.getRotation = require('./getRotation')

exports.getScale = require('./getScale')

exports.getTranslation = require('./getTranslation')

exports.toArray = require('./toArray')

exports.toMatrix = require('./toMatrix')

exports.toString = require('./toString')

exports.point = require('./point')

exports.points = require('./points')

exports.inverse = require('./inverse')

// exports.addVector = ?
// exports.translateBy =
exports.translate = require('./translate')

exports.scale = require('./scale')

exports.rotate = require('./rotate')

exports.compose =
exports.multiply = require('./multiply')
<|endoftext|>