<commit_before>/*
 * slush-kickstart
 * https://github.com/mohamdio/slush-kickstart
 * A slush generator to scaffold front-end projects
 *
 * Copyright (c) 2016, mohamdio
 * Licensed under the MIT license.
 */

'use strict';

var gulp = require('gulp'),
    install = require('gulp-install'),
    conflict = require('gulp-conflict'),
    template = require('gulp-template'),
    rename = require('gulp-rename'),
    _ = require('underscore.string'),
    inquirer = require('inquirer'),
    path = require('path');

function format(string) {
    var username = string.toLowerCase();
    return username.replace(/\s/g, '');
}

var defaults = (function() {
    var workingDirName = path.basename(process.cwd()),
        homeDir, osUserName, configFile, user;

    if (process.platform === 'win32') {
        homeDir = process.env.USERPROFILE;
        osUserName = process.env.USERNAME || path.basename(homeDir).toLowerCase();
    } else {
        homeDir = process.env.HOME || process.env.HOMEPATH;
        osUserName = homeDir && homeDir.split('/').pop() || 'root';
    }

    configFile = path.join(homeDir, '.gitconfig');
    user = {};

    if (require('fs').existsSync(configFile)) {
        user = require('iniparser').parseSync(configFile).user;
    }

    return {
        projectName: workingDirName,
        userName: osUserName || format(user.name || ''),
        authorName: user.name || '',
        authorEmail: user.email || ''
    };
})();

gulp.task('default', function(done) {
    var prompts = [{
        name: 'projectName',
        message: 'What is the name of your project?',
        default: defaults.projectName
    }, {
        name: 'projectDescription',
        message: 'What is the description?'
    }, {
        name: 'projectVersion',
        message: 'What is the version of your project?',
        default: '1.0.0'
    }, {
        name: 'authorName',
        message: 'What is the author name?',
        default: defaults.authorName
    }, {
        name: 'authorEmail',
        message: 'What is the author email?',
        default: defaults.authorEmail
    }, {
        name: 'authorUrl',
        message: 'What is the author url?'
    }, {
        name: 'userName',
        message: 'What is the github username?',
        default: defaults.userName
    }, {
        type: 'list',
        name: 'license',
        message: 'What is the project license? (use arrow keys select)',
        choices: ['MIT', 'BSD'],
        default: 'MIT'
    }, {
        type: 'list',
        name: 'cssFramework',
        message: 'What is the css framework want to use? (use arrow keys select)',
        choices: ['foundation-sites', 'bootstrap', 'bootstrap v4.0.0-alpha'],
        default: 'foundation-sites'
    }, {
        type: 'checkbox',
        name: 'iconFonts',
        message: 'Which icon fonts want to include? (use arrow keys and spacebar to select/deselect)',
        choices: [
            {
            name: 'Font Awesome',
            value: 'fontAwesome',
            checked: true
            }, {
            name: 'Foundation Icon Fonts',
            value: 'foundationIconFonts',
            }, {
            name: 'Ionicons',
            value: 'ionicons',
            }, {
            name: 'IcoMoon',
            value: 'icoMoon',
            }, {
            name: 'Octicons',
            value: 'octicons',
            }, {
            name: 'Typicons',
            value: 'typicons',
            }, {
            name: 'Devicons',
            value: 'devicons',
            }
        ]
    }, {
        type: 'confirm',
        name: 'moveon',
        message: 'Continue?'
    }];
    //Ask
    inquirer.prompt(prompts,
        function(answers) {
            if (!answers.moveon) {
                return done();
            }
            answers.projectNameSlug = _.slugify(answers.projectName);
            var d = new Date();
            answers.year = d.getFullYear();
            gulp.src(__dirname + '/templates/**')
                .pipe(template(answers))
                .pipe(rename(function(file) {
                    if (file.basename[0] === '_') {
                        file.basename = '.' + file.basename.slice(1);
                    }
                }))
                .pipe(conflict('./'))
                .pipe(gulp.dest('./'))
                .pipe(install())
                .on('end', function() {
                    done();
                });
        });
});<commit_msg>filter gulp.src files before dest templates content<commit_after>/*
 * slush-kickstart
 * https://github.com/mohamdio/slush-kickstart
 * A slush generator to scaffold front-end projects
 *
 * Copyright (c) 2016, mohamdio
 * Licensed under the MIT license.
 */

'use strict';

var gulp = require('gulp'),
    install = require('gulp-install'),
    conflict = require('gulp-conflict'),
    template = require('gulp-template'),
    rename = require('gulp-rename'),
    _ = require('underscore.string'),
    inquirer = require('inquirer'),
    path = require('path');

function format(string) {
    var username = string.toLowerCase();
    return username.replace(/\s/g, '');
}

var defaults = (function() {
    var workingDirName = path.basename(process.cwd()),
        homeDir, osUserName, configFile, user;

    if (process.platform === 'win32') {
        homeDir = process.env.USERPROFILE;
        osUserName = process.env.USERNAME || path.basename(homeDir).toLowerCase();
    } else {
        homeDir = process.env.HOME || process.env.HOMEPATH;
        osUserName = homeDir && homeDir.split('/').pop() || 'root';
    }

    configFile = path.join(homeDir, '.gitconfig');
    user = {};

    if (require('fs').existsSync(configFile)) {
        user = require('iniparser').parseSync(configFile).user;
    }

    return {
        projectName: workingDirName,
        userName: osUserName || format(user.name || ''),
        authorName: user.name || '',
        authorEmail: user.email || ''
    };
})();

gulp.task('default', function(done) {
    var prompts = [{
        name: 'projectName',
        message: 'What is the name of your project?',
        default: defaults.projectName
    }, {
        name: 'projectDescription',
        message: 'What is the description?'
    }, {
        name: 'projectVersion',
        message: 'What is the version of your project?',
        default: '1.0.0'
    }, {
        name: 'authorName',
        message: 'What is the author name?',
        default: defaults.authorName
    }, {
        name: 'authorEmail',
        message: 'What is the author email?',
        default: defaults.authorEmail
    }, {
        name: 'authorUrl',
        message: 'What is the author url?'
    }, {
        name: 'userName',
        message: 'What is the github username?',
        default: defaults.userName
    }, {
        type: 'list',
        name: 'license',
        message: 'What is the project license? (use arrow keys select)',
        choices: ['MIT', 'BSD'],
        default: 'MIT'
    }, {
        type: 'list',
        name: 'cssFramework',
        message: 'What is the css framework want to use? (use arrow keys select)',
        choices: ['foundation-sites', 'bootstrap', 'bootstrap v4.0.0-alpha'],
        default: 'foundation-sites'
    }, {
        type: 'checkbox',
        name: 'iconFonts',
        message: 'Which icon fonts want to include? (use arrow keys and spacebar to select/deselect)',
        choices: [
            {
            name: 'Font Awesome',
            value: 'fontAwesome',
            checked: true
            }, {
            name: 'Foundation Icon Fonts',
            value: 'foundationIconFonts',
            }, {
            name: 'Ionicons',
            value: 'ionicons',
            }, {
            name: 'IcoMoon',
            value: 'icoMoon',
            }, {
            name: 'Octicons',
            value: 'octicons',
            }, {
            name: 'Typicons',
            value: 'typicons',
            }, {
            name: 'Devicons',
            value: 'devicons',
            }
        ]
    }, {
        type: 'confirm',
        name: 'moveon',
        message: 'Continue?'
    }];
    //Ask
    inquirer.prompt(prompts,
        function(answers) {
            if (!answers.moveon) {
                return done();
            }
            answers.projectNameSlug = _.slugify(answers.projectName);
            var d = new Date();
            answers.year = d.getFullYear();
            var files = [__dirname + '/templates/**'];
            if (answers.cssFramework === 'foundation-sites') {
                files.push('!' + __dirname + '/templates/source/scss/vendor/bootstrap.scss');
            } else {
                files.push('!' + __dirname + '/templates/source/scss/vendor/foundation-sites.scss');
            }
            if (!answers.fontAwesome) {
                files.push('!' + __dirname + '/templates/source/scss/vendor/font-awesome.scss');
            }
            gulp.src(files)
                .pipe(template(answers))
                .pipe(rename(function(file) {
                    if (file.basename[0] === '_') {
                        file.basename = '.' + file.basename.slice(1);
                    }
                }))
                .pipe(conflict('./'))
                .pipe(gulp.dest('./'))
                .pipe(install())
                .on('end', function() {
                    done();
                });
        });
});<|endoftext|>


<commit_before>var styles = {
    log: "#2a53cd",
    info: "#228B22",
    warn: "#dd6a16",
    error: "#dc352e"
};
var alerts = [];
var clean = Cleanup(alerts);
var build = function () {
    var sum = 0;
    alerts.forEach(function (elem) {
        if (parseInt(elem.style.top) !== sum);
        elem.style.top = sum + "px";
        sum += elem.offsetHeight;
    });
};
var fontSize = 16;
var singleHeight = fontSize * 3.125 | 0;
var _text = function (color, parameters) {
    var box = createElement(div);
    var boxWidth = 720;
    if (boxWidth > innerWidth) {
        boxWidth = +innerWidth;
    }
    css(box, `width:${fromPixel(boxWidth)};top:${fromPixel(singleHeight * alerts.length)};margin-left:-${fromPixel(boxWidth >> 1)};transition:all 0.1s ease-out;left:50%;height:${fromPixel(singleHeight)};line-height:${fromPixel(singleHeight)};font-size:${fromPixel(fontSize)};background-color:${color};position:absolute;color:#fff;text-align:center;`);
    text(box, [].slice.call(parameters, 0).join(", "));
    box.initialStyle = `margin-top:-${fromPixel(singleHeight)};opacity:0;`;
    return box;
};
function alert() {
    var color = String(this), text, autoclose = true, onclose;
    var color_reg = /^#(?:\w{6}|\w{3})$/;
    [].map.call(arguments, function (arg) {
        switch (typeof arg) {
            case "string":
                if (!text) {
                    text = arg;
                } else if (color_reg.test(text)) {
                    color = text;
                    text = arg;
                } else if (text in styles) {
                    color = styles[text];
                    text = arg;
                } else if (arg in styles) {
                    color = styles[arg];
                } else if (color_reg.test(arg)) {
                    color = arg;
                }
                break;
            case "boolean":
            case "number":
                autoclose = arg;
                break;
            case "function":
                onclose = arg;
        }
    });
    var elem;

    if (color_reg.test(color)) {
        elem = _text(color, [text]);
    } else {
        elem = _text(styles.log, arguments);
    }
    var _onclose = function (event) {
        if (onclose) {
            onclose.call(this, event);
        }
        clean(this);
        build();
        if (close_timer) clearTimeout(close_timer);
    }
    onremove(elem, _onclose);
    if (autoclose) {
        if (autoclose === true) {
            autoclose = text.length * 200 + 400;
        } else if (autoclose < 100) {
            autoclose = autoclose * 1000;
        }
        var close_timer = setTimeout(function () {
            remove(elem);
        }, autoclose);
    }
    alerts.push(elem);
    popup(elem);
    return elem;
}
for (var k in styles) {
    alert[k] = new Function(`return this.apply(${JSON.stringify(styles[k])},arguments);`);
}
<commit_msg>顶部alert框自动居中<commit_after>var styles = {
    log: "#2a53cd",
    info: "#228B22",
    warn: "#dd6a16",
    error: "#dc352e"
};
var alerts = [];
var clean = Cleanup(alerts);
var build = function () {
    var sum = 0;
    alerts.forEach(function (elem) {
        if (parseInt(elem.style.top) !== sum);
        elem.style.top = sum + "px";
        sum += elem.offsetHeight;
    });
};
var fontSize = 16;
var singleHeight = fontSize * 3.125 | 0;
var _text = function (color, parameters) {
    var box = createElement(div);
    css(box, `top:${fromPixel(singleHeight * alerts.length)};height:${fromPixel(singleHeight)};line-height:${fromPixel(singleHeight)};font-size:${fromPixel(fontSize)};background-color:${color};max-width: 720px; left:0;right:0;margin:0 auto;transition: all 0.2s ease-out;position:absolute;color:#fff;text-align:center;`);
    text(box, [].slice.call(parameters, 0).join(", "));
    box.initialStyle = `margin:-${fromPixel(singleHeight)} auto;opacity:0;`;
    return box;
};
function alert() {
    var color = String(this), text, autoclose = true, onclose;
    var color_reg = /^#(?:\w{6}|\w{3})$/;
    [].map.call(arguments, function (arg) {
        switch (typeof arg) {
            case "string":
                if (!text) {
                    text = arg;
                } else if (color_reg.test(text)) {
                    color = text;
                    text = arg;
                } else if (text in styles) {
                    color = styles[text];
                    text = arg;
                } else if (arg in styles) {
                    color = styles[arg];
                } else if (color_reg.test(arg)) {
                    color = arg;
                }
                break;
            case "boolean":
            case "number":
                autoclose = arg;
                break;
            case "function":
                onclose = arg;
        }
    });
    var elem;

    if (color_reg.test(color)) {
        elem = _text(color, [text]);
    } else {
        elem = _text(styles.log, arguments);
    }
    var _onclose = function (event) {
        if (onclose) {
            onclose.call(this, event);
        }
        clean(this);
        build();
        if (close_timer) clearTimeout(close_timer);
    }
    onremove(elem, _onclose);
    if (autoclose) {
        if (autoclose === true) {
            autoclose = text.length * 200 + 400;
        } else if (autoclose < 100) {
            autoclose = autoclose * 1000;
        }
        var close_timer = setTimeout(function () {
            remove(elem);
        }, autoclose);
    }
    alerts.push(elem);
    popup(elem);
    return elem;
}
for (var k in styles) {
    alert[k] = new Function(`return this.apply(${JSON.stringify(styles[k])},arguments);`);
}
<|endoftext|>


<commit_before>// Copyright 2014-2020, University of Colorado Boulder

/**
 * Main file for the scenery-phet library demo.
 *
 * @author Sam Reid
 */

import Property from '../../axon/js/Property.js';
import Screen from '../../joist/js/Screen.js';
import ScreenIcon from '../../joist/js/ScreenIcon.js';
import Sim from '../../joist/js/Sim.js';
import simLauncher from '../../joist/js/simLauncher.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Tandem from '../../tandem/js/Tandem.js';
import ButtonsScreenView from './demo/ButtonsScreenView.js';
import ComponentsScreenView from './demo/ComponentsScreenView.js';
import DialogsScreenView from './demo/DialogsScreenView.js';
import SceneryPhetMemoryTestsScreenView from './demo/SceneryPhetMemoryTestsScreenView.js';
import SceneryPhetKeyboardHelpContent from './demo/SceneryPhetKeyboardHelpContent.js';
import SlidersScreenView from './demo/SlidersScreenView.js';
import SpringScreenView from './demo/SpringScreenView.js';
import sceneryPhetStrings from './sceneryPhetStrings.js';
import sceneryPhetQueryParameters from './sceneryPhetQueryParameters.js';

// empty model used for all demo screens
const MODEL = {};

const simOptions = {
  credits: {
    leadDesign: 'PhET'
  },
  keyboardHelpNode: new SceneryPhetKeyboardHelpContent(),
  webgl: true
};

/**
 * Creates a simple screen icon, a colored rectangle.
 * @param {ColorDef} color
 * @returns {ScreenIcon}
 */
function createScreenIcon( color ) {
  return new ScreenIcon(
    new Rectangle( 0, 0, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.width, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.height, {
      fill: color
    } )
  );
}

// Create and start sim
simLauncher.launch( () => {

  const componentsScreenTandem = Tandem.ROOT.createTandem( 'componentsScreen' );

  new Sim( sceneryPhetStrings[ 'scenery-phet' ].title, [

    // Buttons screen
    new Screen(
      () => MODEL,
      () => new ButtonsScreenView(),
      {
        name: 'Buttons',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'red' ),
        tandem: Tandem.ROOT.createTandem( 'buttonsScreen' )
      }
    ),

    // Sliders screen
    new Screen(
      () => MODEL,
      () => new SlidersScreenView(),
      {
        name: 'Sliders',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'yellow' ),
        tandem: Tandem.ROOT.createTandem( 'slidersScreen' )
      }
    ),

    // Components screen
    new Screen(
      () => MODEL,
      () => new ComponentsScreenView( { tandem: componentsScreenTandem.createTandem( 'view' ) } ),
      {
        name: 'Components',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'orange' ),
        tandem: componentsScreenTandem
      }
    ),

    // Dialogs screen
    new Screen(
      () => MODEL,
      () => new DialogsScreenView(),
      {
        name: 'Dialogs',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'maroon' ),
        tandem: Tandem.ROOT.createTandem( 'dialogsScreen' )
      }
    ),

    // Spring screen
    new Screen(
      () => MODEL,
      () => new SpringScreenView(),
      {
        name: 'Spring',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'blue' ),
        tandem: Tandem.ROOT.createTandem( 'springsScreen' )
      }
    ),

    // Memory Tests screen
    new Screen(
      () => MODEL,
      () => new SceneryPhetMemoryTestsScreenView(),
      {
        name: 'Memory Tests',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'purple' ),
        tandem: Tandem.ROOT.createTandem( 'memoryTestsScreen' )
      }
    )
  ], simOptions ).start();
} );<commit_msg>refactor sims to use screen-based keyboard help content, https://github.com/phetsims/joist/issues/668<commit_after>// Copyright 2014-2020, University of Colorado Boulder

/**
 * Main file for the scenery-phet library demo.
 *
 * @author Sam Reid
 */

import Property from '../../axon/js/Property.js';
import Screen from '../../joist/js/Screen.js';
import ScreenIcon from '../../joist/js/ScreenIcon.js';
import Sim from '../../joist/js/Sim.js';
import simLauncher from '../../joist/js/simLauncher.js';
import Rectangle from '../../scenery/js/nodes/Rectangle.js';
import Tandem from '../../tandem/js/Tandem.js';
import ButtonsScreenView from './demo/ButtonsScreenView.js';
import ComponentsScreenView from './demo/ComponentsScreenView.js';
import DialogsScreenView from './demo/DialogsScreenView.js';
import SceneryPhetKeyboardHelpContent from './demo/SceneryPhetKeyboardHelpContent.js';
import SceneryPhetMemoryTestsScreenView from './demo/SceneryPhetMemoryTestsScreenView.js';
import SlidersScreenView from './demo/SlidersScreenView.js';
import SpringScreenView from './demo/SpringScreenView.js';
import sceneryPhetQueryParameters from './sceneryPhetQueryParameters.js';
import sceneryPhetStrings from './sceneryPhetStrings.js';

// empty model used for all demo screens
const MODEL = {};

const simOptions = {
  credits: {
    leadDesign: 'PhET'
  },
  hasKeyboardHelpContent: true,
  webgl: true
};

const keyboardHelpNode = new SceneryPhetKeyboardHelpContent();

/**
 * Creates a simple screen icon, a colored rectangle.
 * @param {ColorDef} color
 * @returns {ScreenIcon}
 */
function createScreenIcon( color ) {
  return new ScreenIcon(
    new Rectangle( 0, 0, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.width, Screen.MINIMUM_HOME_SCREEN_ICON_SIZE.height, {
      fill: color
    } )
  );
}

// Create and start sim
simLauncher.launch( () => {

  const componentsScreenTandem = Tandem.ROOT.createTandem( 'componentsScreen' );

  new Sim( sceneryPhetStrings[ 'scenery-phet' ].title, [

    // Buttons screen
    new Screen(
      () => MODEL,
      () => new ButtonsScreenView(),
      {
        name: 'Buttons',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'red' ),
        tandem: Tandem.ROOT.createTandem( 'buttonsScreen' ),
        keyboardHelpNode: keyboardHelpNode
      }
    ),

    // Sliders screen
    new Screen(
      () => MODEL,
      () => new SlidersScreenView(),
      {
        name: 'Sliders',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'yellow' ),
        tandem: Tandem.ROOT.createTandem( 'slidersScreen' ),
        keyboardHelpNode: keyboardHelpNode
      }
    ),

    // Components screen
    new Screen(
      () => MODEL,
      () => new ComponentsScreenView( { tandem: componentsScreenTandem.createTandem( 'view' ) } ),
      {
        name: 'Components',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'orange' ),
        tandem: componentsScreenTandem,
        keyboardHelpNode: keyboardHelpNode
      }
    ),

    // Dialogs screen
    new Screen(
      () => MODEL,
      () => new DialogsScreenView(),
      {
        name: 'Dialogs',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'maroon' ),
        tandem: Tandem.ROOT.createTandem( 'dialogsScreen' ),
        keyboardHelpNode: keyboardHelpNode
      }
    ),

    // Spring screen
    new Screen(
      () => MODEL,
      () => new SpringScreenView(),
      {
        name: 'Spring',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'blue' ),
        tandem: Tandem.ROOT.createTandem( 'springsScreen' ),
        keyboardHelpNode: keyboardHelpNode
      }
    ),

    // Memory Tests screen
    new Screen(
      () => MODEL,
      () => new SceneryPhetMemoryTestsScreenView(),
      {
        name: 'Memory Tests',
        backgroundColorProperty: new Property( sceneryPhetQueryParameters.backgroundColor ),
        homeScreenIcon: createScreenIcon( 'purple' ),
        tandem: Tandem.ROOT.createTandem( 'memoryTestsScreen' ),
        keyboardHelpNode: keyboardHelpNode
      }
    )
  ], simOptions ).start();
} );<|endoftext|>


<commit_before>const Telegraf = require('telegraf');
const {Extra, Markup} = require('telegraf');

const config = require('../config');
const dataService = require('./dataService');
const api = require('../api/stuv_scraper.js');

const bot = new Telegraf(config.botToken);

//get username for group command handling
bot.telegram.getMe().then((botInfo) => {
    bot.options.username = botInfo.username;
    console.log("Initialized", botInfo.username);
});

dataService.loadUsers();

//TODO: message logging
//TODO: get courses from api

/*
 setTimeout(()=>{ //dataService debug code
 console.log(dataService.getCoursesToFetch());
 console.log(dataService.getSubscribersOfCourse("INF16A"));
 }, 100);*/

let mockCourses = [];
/*
 available states:
 - selectCourseType
 - selectCourse
 */

function userString(msg) {
    return JSON.stringify(msg.from.id == msg.chat.id ? msg.from : {
        from: msg.from,
        chat: msg.chat
    });
}

function logMsg(msg) {
    const from = userString(msg);
    console.log(msg.message.text, from)
}

function logOutMsg(msg, text) {
    console.log('>', {
        id: msg.chat.id
    }, text);
}

function getCourseTypes() {
    const courseTypes = [];
    mockCourses.forEach((val) => {
        const res = val.match(/[^0-9]*/)[0];
        if (courseTypes.indexOf(res) == -1)
            courseTypes.push(res);
    });
    return courseTypes;
}

function getCoursesByType(type) {
    return mockCourses.filter((val) => val.indexOf(type) > -1);
}

function courseTypeSelection(msg) { //display the course type selection
    logMsg(msg);
    const inlineData = [];
    const rowCnt = 4;
    let tmp = [];
    getCourseTypes().forEach((val, i) => {
        if (i % rowCnt == 0) {
            tmp = [];
        }
        tmp.push(Markup.callbackButton(val, val));
        if (i % rowCnt == rowCnt - 1) {
            inlineData.push(tmp);
        }
    });
    dataService.setMetaData(msg.chat.id, "state", "selectCourseType");
    return msg.reply('Bitte wähle ein Kurstyp aus der Liste.',
        Extra.HTML().markup(Markup.inlineKeyboard(inlineData)));
}

function courseSelection(msg, type) { //display the course selection (after selecting a type)
    const uid = msg.chat.id;
    const inlineData = [];
    const rowCnt = 3;
    let tmp = [];
    const courses = getCoursesByType(type);
    courses.forEach((val, i) => {
        if (i % rowCnt == 0) {
            tmp = [];
        }
        tmp.push(Markup.callbackButton(val, val));
        if (i % rowCnt == rowCnt - 1 || i == courses.length - 1)
            inlineData.push(tmp);
    });
    dataService.setMetaData(uid, "state", "selectCourse");
    bot.telegram.sendMessage(uid, 'Nun wähle einen Kurs.',
        Extra.HTML().markup(Markup.inlineKeyboard(inlineData)));
}
/*
function publish(data) { //expects data in form of {'courseName': "stringToPublish", ...}
    Object.keys(data).forEach(key => {
        const val = data[key];

    });
}*/

bot.command('start', (msg) => {
    //console.log('start', msg.from, msg.chat);
    //logMsg(msg); //commented out because will already get logged in courseTypeSelection
    dataService.registerUser(msg);
    const m = 'Willkommen! Ich bin der DHBW Mosbach Vorlesungsplan-Bot. Ich sende dir jeden Morgen den aktuellen Vorlesungsplan zu.';
    msg.reply(m);
    logOutMsg(msg, m);
    courseTypeSelection(msg);
});

bot.command('stop', (msg) => {
    logMsg(msg);
    dataService.setEnabled(msg.chat.id, false);
    const m = 'Ich sende nun nicht mehr jeden Tag den neuen Vorlesungsplan';
    logOutMsg(msg, m);
    msg.reply(m);
});

bot.command('changeclass', courseTypeSelection); //call method with parameter msg

bot.command('getplan', msg => {
    logMsg(msg);
    const course = dataService.getCourse(msg.chat.id);
    const day = new Date(); //ToDo: maybe custom date handling (let user pick date from inline calendar)
    day.setHours(day.getHours() + 6); //if it's after 18:00, get next day
    api.getPlan(course, day)
        .then((data) => {
            const m = '
' + data + '
';
            logOutMsg(msg, m);
            msg.replyWithHTML(m);
        });
});

bot.action(/.+/, (msg) => {
    const answer = msg.match[0];
    const uid = msg.chat.id;
    const curState = dataService.getMetaData(uid, "state");

    if (curState == "selectCourseType") {
        courseSelection(msg, answer);
    } else if (curState == "selectCourse") {
        msg.replyWithHTML("Danke! Ich werde nun den Kurs " + answer + " benutzen.");

        dataService.setCourse(uid, answer);
        dataService.setMetaData(uid, "state", undefined);
    }
    msg.deleteMessage();
    console.log('[' + answer + ']', curState ? "" : "IGNORED", userString(msg));
});

api.getCourses().then((courses) => {
    mockCourses = courses;
    bot.startPolling();
});

module.exports = {}<commit_msg>Emergency fix breaking bug<commit_after>const Telegraf = require('telegraf');
const {Extra, Markup} = require('telegraf');

const config = require('../config');
const dataService = require('./dataService');
const api = require('../api/stuv_scraper.js');

const bot = new Telegraf(config.botToken);

//get username for group command handling
bot.telegram.getMe().then((botInfo) => {
    bot.options.username = botInfo.username;
    console.log("Initialized", botInfo.username);
});

dataService.loadUsers();

//TODO: message logging
//TODO: get courses from api

/*
 setTimeout(()=>{ //dataService debug code
 console.log(dataService.getCoursesToFetch());
 console.log(dataService.getSubscribersOfCourse("INF16A"));
 }, 100);*/

let mockCourses = [];
/*
 available states:
 - selectCourseType
 - selectCourse
 */

function userString(msg) {
    return JSON.stringify(msg.from.id == msg.chat.id ? msg.from : {
        from: msg.from,
        chat: msg.chat
    });
}

function logMsg(msg) {
    const from = userString(msg);
    console.log(msg.message.text, from)
}

function logOutMsg(msg, text) {
    console.log('>', {
        id: msg.chat.id
    }, text);
}

function getCourseTypes() {
    const courseTypes = [];
    mockCourses.forEach((val) => {
        const res = val.match(/[^0-9]*/)[0];
        if (courseTypes.indexOf(res) == -1)
            courseTypes.push(res);
    });
    return courseTypes;
}

function getCoursesByType(type) {
    return mockCourses.filter((val) => val.indexOf(type) > -1);
}

function courseTypeSelection(msg) { //display the course type selection
    logMsg(msg);
    const inlineData = [];
    const rowCnt = 4;
    let tmp = [];
    getCourseTypes().forEach((val, i) => {
        if (i % rowCnt == 0) {
            tmp = [];
        }
        tmp.push(Markup.callbackButton(val, val));
        if (i % rowCnt == rowCnt - 1) {
            inlineData.push(tmp);
        }
    });
    dataService.setMetaData(msg.chat.id, "state", "selectCourseType");
    return msg.reply('Bitte wähle ein Kurstyp aus der Liste.',
        Extra.HTML().markup(Markup.inlineKeyboard(inlineData)));
}

function courseSelection(msg, type) { //display the course selection (after selecting a type)
    const uid = msg.chat.id;
    const inlineData = [];
    const rowCnt = 3;
    let tmp = [];
    const courses = getCoursesByType(type);
    courses.forEach((val, i) => {
        if (i % rowCnt == 0) {
            tmp = [];
        }
        tmp.push(Markup.callbackButton(val, val));
        if (i % rowCnt == rowCnt - 1 || i == courses.length - 1)
            inlineData.push(tmp);
    });
    dataService.setMetaData(uid, "state", "selectCourse");
    bot.telegram.sendMessage(uid, 'Nun wähle einen Kurs.',
        Extra.HTML().markup(Markup.inlineKeyboard(inlineData)));
}
/*
function publish(data) { //expects data in form of {'courseName': "stringToPublish", ...}
    Object.keys(data).forEach(key => {
        const val = data[key];

    });
}*/

bot.command('start', (msg) => {
    //console.log('start', msg.from, msg.chat);
    //logMsg(msg); //commented out because will already get logged in courseTypeSelection
    dataService.registerUser(msg);
    const m = 'Willkommen! Ich bin der DHBW Mosbach Vorlesungsplan-Bot. Ich sende dir jeden Morgen den aktuellen Vorlesungsplan zu.';
    msg.reply(m);
    logOutMsg(msg, m);
    courseTypeSelection(msg);
});

bot.command('stop', (msg) => {
    logMsg(msg);
    dataService.setEnabled(msg.chat.id, false);
    const m = 'Ich sende nun nicht mehr jeden Tag den neuen Vorlesungsplan';
    logOutMsg(msg, m);
    msg.reply(m);
});

bot.command('changeclass', courseTypeSelection); //call method with parameter msg

bot.command('getplan', msg => {
    logMsg(msg);
    const course = dataService.getCourse(msg.chat.id);
    const day = new Date(); //ToDo: maybe custom date handling (let user pick date from inline calendar)
    day.setHours(day.getHours() + 6); //if it's after 18:00, get next day
    api.getPlan(course, day)
        .then((data) => {
            const m = '
' + data + '
';
            logOutMsg(msg, m);
            msg.replyWithHTML(m);
        });
});

bot.action(/.+/, (msg) => {
    const answer = msg.match[0];
    const uid = msg.chat.id;
    const curState = dataService.getMetaData(uid, "state");

    if (curState == "selectCourseType") {
        courseSelection(msg, answer);
    } else if (curState == "selectCourse") {
        msg.replyWithHTML("Danke! Ich werde nun den Kurs " + answer + " benutzen.");

        dataService.setCourse(uid, answer);
        dataService.setMetaData(uid, "state", undefined);
    }
    console.log('[' + answer + ']', curState ? "" : "IGNORED", userString(msg));
});

api.getCourses().then((courses) => {
    mockCourses = courses;
    bot.startPolling();
});

module.exports = {}
<|endoftext|>


<commit_before>function field() {
    var result = option.apply(null, arguments);
    result.removeAttribute("tabIndex");
    return result;
}<commit_msg>tabIndex改成tabindex<commit_after>function field() {
    var result = option.apply(null, arguments);
    result.removeAttribute("tabindex");
    return result;
}<|endoftext|>


<commit_before>// Copyright 2016 Endless Mobile, Inc.

const Format = imports.format;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Soup = imports.gi.Soup;

const AsyncTask = imports.search.asyncTask;
const Config = imports.search.config;
const Utils = imports.search.utils;

const FileDownloadRequest = new Lang.Class({
    Name: 'FileDownloadRequest',

    _init: function (uri, out_file, csum) {
        this.uri = uri;
        this.out_file = out_file;
        this.csum = csum;
    },
});

const FileDownloader = new Lang.Class({
    Name: 'FileDownloader',

    _init: function () {
        this._http_session = new Soup.Session({ user_agent: 'ekn-downloader ',
                                                max_conns_per_host: 8 });

        // XXX: https://bugzilla.gnome.org/show_bug.cgi?id=655189
        // this._session.add_feature(new Soup.ProxyResolverDefault());
        Soup.Session.prototype.add_feature.call(this._http_session, new Soup.ProxyResolverDefault());

        this._requester = new Soup.Requester();
        Soup.Session.prototype.add_feature.call(this._http_session, this._requester);
    },

    verify_checksum: function (file, against_digest, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);
        task.catch_errors(() => {
            let stream = file.read(cancellable);
            let csum = new GLib.Checksum(GLib.ChecksumType.SHA256);

            function finish() {
                let digest = csum.get_string();
                if (against_digest.toLowerCase() !== digest.toLowerCase())
                    throw new Error(Format.vprintf("Checksum against file %s did not match: %s (local), %s (remote)", [file.get_basename(), digest, against_digest]));
                task.return_value(true);
            }

            function bytes_read(stream, res) {
                let bytes = stream.read_bytes_finish(res);
                csum.update(bytes.get_data());

                if (bytes.get_size() !== 0)
                    continue_checksum();
                else
                    finish();
            }

            function continue_checksum() {
                stream.read_bytes_async(Utils.CHUNK_SIZE, 0, cancellable, task.catch_callback_errors(bytes_read));
            }

            continue_checksum();
        });
        return task;
    },

    verify_checksum_finish: function (task) {
        return task.finish();
    },

    download_file: function (file_download_request, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);
        task.catch_errors(() => {
            let uri = file_download_request.uri;
            let out_file = file_download_request.out_file;

            let download_cancellable = new Gio.Cancellable();
            if (cancellable)
                cancellable.connect(() => {
                    download_cancellable.cancel();
                });

            function keep_new_file() {
                tmp_file.move(out_file, Gio.FileCopyFlags.OVERWRITE, cancellable, null);
                task.return_value(out_file);
            }

            function keep_existing_file() {
                download_cancellable.cancel();
                tmp_file.delete(cancellable);
                task.return_value(out_file);
            }

            let finish_download = (finish) => {
                // If we have a checksum, verify it.
                let csum = file_download_request.csum;

                if (csum) {
                    this.verify_checksum(tmp_file, csum, cancellable, task.catch_callback_errors((source, csum_task) => {
                        // XXX: If the file didn't checksum correctly, what should we do? Trash and re-download?
                        this.verify_checksum_finish(csum_task);
                        finish();
                    }));
                } else {
                    finish();
                }
            };

            let request = this._requester.request_uri(uri);

            let tmp_file = out_file.get_parent().get_child(out_file.get_basename() + '.part');

            if (tmp_file.query_exists(cancellable)) {
                let size = tmp_file.query_info('standard::size', Gio.FileQueryInfoFlags.NONE, cancellable).get_size();
                if (size > 0)
                    request.get_message().request_headers.set_range(size, -1);
            }

            request.send_async(download_cancellable, task.catch_callback_errors((request, result) => {
                // mirrors the SOUP_STATUS_IS_SUCCESSFUL macro
                function soup_status_is_successful(status) {
                    return status >= 200 && status < 300;
                }

                let body = request.send_finish(result);
                let msg = request.get_message();
                let status = msg.status_code;

                if (status === Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE) {
                    // The server is telling us we already have the full file range.
                    finish_download(keep_new_file);
                } else if (!soup_status_is_successful(status)) {
                    throw new Error("Server returned code " + status + " while fetching " + uri.to_string(true));
                } else {
                    let out_stream;
                    if (status === Soup.Status.PARTIAL_CONTENT) {
                        out_stream = tmp_file.append_to(Gio.FileCreateFlags.NONE, cancellable);
                    } else {
                        out_stream = tmp_file.replace(null, false, Gio.FileCreateFlags.NONE, cancellable);
                    }

                    out_stream.splice_async(body, Gio.OutputStreamSpliceFlags.NONE,
                                            GLib.PRIORITY_DEFAULT, cancellable, task.catch_callback_errors((source, res) => {
                                                out_stream.splice_finish(res);
                                                out_stream.close(cancellable);
                                                finish_download(keep_new_file);
                                            }));
                }
            }));
        });

        return task;
    },

    download_file_finish: function (task) {
        return task.finish();
    },
});

// Returns items that are in A but not in B.
function subtract_set (A, B, key) {
    let result = [];
    let mapped_A = A.map(key);
    let mapped_B = B.map(key);

    for (let i = 0; i < mapped_A.length; i++) {
        let item = mapped_A[i];
        if (mapped_B.indexOf(item) === -1)
            result.push(A[i]);
    }
    return result;
}

function load_manifest (file, cancellable) {
    try {
        let [success, data] = file.load_contents(cancellable);
        return JSON.parse(data);
    } catch (e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
        return null;
    }
}

const SubscriptionDownloader = new Lang.Class({
    Name: 'SubscriptionDownloader',

    _init: function () {
        this._file_downloader = new FileDownloader();

        this._subscriptions_dir = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), 'com.endlessm.subscriptions']));
        Utils.ensure_directory(this._subscriptions_dir);

        this._load_from_config();
    },

    _load_from_config: function () {
        let config_env = GLib.getenv('EKN_UPDATER_CONFIG');
        if (config_env === null)
            config_env = Config.PKGDATADIR + '/downloader.ini';

        let kf = GLib.KeyFile.new();
        kf.load_from_file(config_env, GLib.KeyFileFlags.NONE);
        this._server_host = kf.get_string("Repository", "ServerUrl");
    },

    _download_new_shards: function (directory, old_manifest, new_manifest, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        let old_shards = old_manifest.shards;
        // Verify that client shards actually appear on disk.
        old_shards = old_shards.filter((shard) => {
            let shard_file = directory.get_child(shard.path);
            return shard_file.query_exists(cancellable);
        });

        let new_shards = new_manifest.shards;

        // Download any shards that are not on the client.

        // XXX: We're currently indexing by filename, assuming it will be good enough.
        // Will it be, or should we also store e.g. ETag?
        let to_download = subtract_set(new_shards, old_shards, (shard) => shard.path);

        AsyncTask.all(this, (add_task) => {
            to_download.forEach((shard) => {
                let shard_file = directory.get_child(shard.path);
                let shard_uri = Soup.URI.new(shard.download_uri);
                let shard_csum = shard.sha256_csum;

                let request = new FileDownloadRequest(shard_uri, shard_file, shard_csum);
                add_task((cancellable, callback) => this._file_downloader.download_file(request, cancellable, callback),
                         (task) => this._file_downloader.download_file_finish(task));
            });
        }, cancellable, task.catch_callback_errors((source, download_shards_task) => {
            AsyncTask.all_finish(download_shards_task);

            task.return_value(true);
        }));

        return task;
    },

    _download_new_shards_finish: function (task) {
        return task.finish();
    },

    apply_update: function (subscription_id, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        task.catch_errors(() => {
            let directory = this._subscriptions_dir.get_child(subscription_id);
            if (!directory.query_exists(cancellable))
                return task.return_value(false);

            let new_manifest_file = directory.get_child('manifest.json.new');
            let new_manifest = load_manifest(new_manifest_file, cancellable);
            // If we don't have a new manifest, we don't have an update.
            if (new_manifest === null)
                return task.return_value(false);

            let manifest_file = directory.get_child('manifest.json');
            let old_manifest = load_manifest(manifest_file, cancellable);
            if (old_manifest === null)
                old_manifest = { shards: [] };

            // Make sure we have all shards before we can apply the update.
            let new_shards = new_manifest.shards;
            let have_all_shards = new_shards.every((shard) => {
                let shard_file = directory.get_child(shard.path);
                return shard_file.query_exists(cancellable);
            });

            if (!have_all_shards)
                return task.return_value(false);

            // OK, we're ready to go and can apply the update. Move our new manifest,
            // and then clean up the old shards.
            new_manifest_file.move(manifest_file, Gio.FileCopyFlags.OVERWRITE, cancellable, null);

            let old_shards = old_manifest.shards;
            let new_shards = new_manifest.shards;
            let to_delete = subtract_set(old_shards, new_shards, (shard) => shard.path);

            to_delete.forEach((shard) => {
                let shard_file = directory.get_child(shard.path);
                shard_file.delete(cancellable);
            });

            return task.return_value(true);
        });

        return task;
    },

    apply_update_finish: function (task) {
        return task.finish();
    },

    fetch_update: function (subscription_id, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        task.catch_errors(() => {
            let directory = this._subscriptions_dir.get_child(subscription_id);
            Utils.ensure_directory(directory);

            let manifest_file = directory.get_child('manifest.json');

            let old_manifest = load_manifest(manifest_file, cancellable);
            if (old_manifest === null)
                old_manifest = { shards: [] };

            let new_manifest_file = directory.get_child('manifest.json.new');
            let uri = Format.vprintf('%s/v1/%s/manifest.json', [this._server_host, subscription_id]);
            let manifest_uri = Soup.URI.new(uri);

            let request = new FileDownloadRequest(manifest_uri, new_manifest_file);
            this._file_downloader.download_file(request, cancellable, task.catch_callback_errors((source, download_task) => {
                this._file_downloader.download_file_finish(download_task);

                let new_manifest = load_manifest(new_manifest_file, cancellable);
                this._download_new_shards(directory, old_manifest, new_manifest, cancellable, task.catch_callback_errors((source, download_task) => {
                    this._download_new_shards_finish(download_task);
                    task.return_value(true);
                }));
            }));
        });

        return task;
    },

    fetch_update_finish: function (task) {
        return task.finish();
    },
});

const DownloaderIface = '\
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> \
  <interface name="com.endlessm.EknDownloader"> \
    <method name = "ApplyUpdate"> \
      <arg type="s" name="subscription_id" direction="in" /> \
      <arg type="b" name="applied_update" direction="out" /> \
    </method> \
    <method name = "FetchUpdate"> \
      <arg type="s" name="subscription_id" direction="in" /> \
    </method> \
  </interface> \
</node>';
const DownloaderIfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(DownloaderIface);

function return_to_dbus (invocation, finish) {
    try {
        let value = finish();
        if (value === undefined)
            value = new GLib.Variant('()', []);
        invocation.return_value(value);
    } catch (e if e instanceof GLib.Error) {
        logError(e, "Exception in DBus call");
        invocation.return_gerror(e);
    } catch (e) {
        let name = e.name;
        if (name.indexOf('.') === -1) {
            // likely to be a normal JS error
            name = 'org.gnome.gjs.JSError.' + name;
        }
        logError(e, "Exception in DBus call");
        invocation.return_dbus_error(name, e.message);
    }
}

const DownloaderService = new Lang.Class({
    Name: 'DownloaderService',

    _init: function (app) {
        this._app = app;
        this._skeleton = Gio.DBusExportedObject.wrapJSObject(DownloaderIfaceInfo, this);
        this._downloader = new SubscriptionDownloader();
    },

    register: function (connection, path) {
        this._skeleton.export(connection, path);
    },

    unregister: function (connection, path) {
        this._skeleton.unexport(connection, path);
    },

    ApplyUpdateAsync: function (params, invocation) {
        let [subscription_id] = params;
        this._app.hold();
        return this._downloader.apply_update(subscription_id, null, (source, task) => {
            return_to_dbus(invocation, () => {
                let res = this._downloader.apply_update_finish(task);
                return new GLib.Variant('(b)', [res]);
            });
            this._app.release();
        });
    },

    FetchUpdateAsync: function (params, invocation) {
        let [subscription_id] = params;
        this._app.hold();
        return this._downloader.fetch_update(subscription_id, null, (source, task) => {
            return_to_dbus(invocation, () => {
                this._downloader.fetch_update_finish(task);
            });
            this._app.release();
        });
    },
});

const Application = new Lang.Class({
    Name: 'EknDownloaderApplication',
    Extends: Gio.Application,

    _init: function () {
        this.parent({ application_id: 'com.endlessm.EknDownloader',
                      flags: Gio.ApplicationFlags.IS_SERVICE,
                      inactivity_timeout: 12000 });

        this._service = new DownloaderService(this);
    },

    vfunc_dbus_register: function (connection, path) {
        this.parent(connection, path);
        this._service.register(connection, path);
        return true;
    },

    vfunc_dbus_unregister: function (connection, path) {
        this.parent(connection, path);
        this._service.unregister(connection, path);
    },
});
<commit_msg>downloader: Don't return an error when we can't access the manifest<commit_after>// Copyright 2016 Endless Mobile, Inc.

const Format = imports.format;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Soup = imports.gi.Soup;

const AsyncTask = imports.search.asyncTask;
const Config = imports.search.config;
const Utils = imports.search.utils;

const FileDownloadRequest = new Lang.Class({
    Name: 'FileDownloadRequest',

    _init: function (uri, out_file, csum) {
        this.uri = uri;
        this.out_file = out_file;
        this.csum = csum;
    },
});

const FileDownloader = new Lang.Class({
    Name: 'FileDownloader',

    _init: function () {
        this._http_session = new Soup.Session({ user_agent: 'ekn-downloader ',
                                                max_conns_per_host: 8 });

        // XXX: https://bugzilla.gnome.org/show_bug.cgi?id=655189
        // this._session.add_feature(new Soup.ProxyResolverDefault());
        Soup.Session.prototype.add_feature.call(this._http_session, new Soup.ProxyResolverDefault());

        this._requester = new Soup.Requester();
        Soup.Session.prototype.add_feature.call(this._http_session, this._requester);
    },

    verify_checksum: function (file, against_digest, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);
        task.catch_errors(() => {
            let stream = file.read(cancellable);
            let csum = new GLib.Checksum(GLib.ChecksumType.SHA256);

            function finish() {
                let digest = csum.get_string();
                if (against_digest.toLowerCase() !== digest.toLowerCase())
                    throw new Error(Format.vprintf("Checksum against file %s did not match: %s (local), %s (remote)", [file.get_basename(), digest, against_digest]));
                task.return_value(true);
            }

            function bytes_read(stream, res) {
                let bytes = stream.read_bytes_finish(res);
                csum.update(bytes.get_data());

                if (bytes.get_size() !== 0)
                    continue_checksum();
                else
                    finish();
            }

            function continue_checksum() {
                stream.read_bytes_async(Utils.CHUNK_SIZE, 0, cancellable, task.catch_callback_errors(bytes_read));
            }

            continue_checksum();
        });
        return task;
    },

    verify_checksum_finish: function (task) {
        return task.finish();
    },

    download_file: function (file_download_request, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);
        task.catch_errors(() => {
            let uri = file_download_request.uri;
            let out_file = file_download_request.out_file;

            let download_cancellable = new Gio.Cancellable();
            if (cancellable)
                cancellable.connect(() => {
                    download_cancellable.cancel();
                });

            function keep_new_file() {
                tmp_file.move(out_file, Gio.FileCopyFlags.OVERWRITE, cancellable, null);
                task.return_value(out_file);
            }

            function keep_existing_file() {
                download_cancellable.cancel();
                tmp_file.delete(cancellable);
                task.return_value(out_file);
            }

            let finish_download = (finish) => {
                // If we have a checksum, verify it.
                let csum = file_download_request.csum;

                if (csum) {
                    this.verify_checksum(tmp_file, csum, cancellable, task.catch_callback_errors((source, csum_task) => {
                        // XXX: If the file didn't checksum correctly, what should we do? Trash and re-download?
                        this.verify_checksum_finish(csum_task);
                        finish();
                    }));
                } else {
                    finish();
                }
            };

            let request = this._requester.request_uri(uri);

            let tmp_file = out_file.get_parent().get_child(out_file.get_basename() + '.part');

            if (tmp_file.query_exists(cancellable)) {
                let size = tmp_file.query_info('standard::size', Gio.FileQueryInfoFlags.NONE, cancellable).get_size();
                if (size > 0)
                    request.get_message().request_headers.set_range(size, -1);
            }

            request.send_async(download_cancellable, task.catch_callback_errors((request, result) => {
                // mirrors the SOUP_STATUS_IS_SUCCESSFUL macro
                function soup_status_is_successful(status) {
                    return status >= 200 && status < 300;
                }

                let body = request.send_finish(result);
                let msg = request.get_message();
                let status = msg.status_code;

                if (status === Soup.Status.REQUESTED_RANGE_NOT_SATISFIABLE) {
                    // The server is telling us we already have the full file range.
                    finish_download(keep_new_file);
                } else if (!soup_status_is_successful(status)) {
                    throw new Error("Server returned code " + status + " while fetching " + uri.to_string(true));
                } else {
                    let out_stream;
                    if (status === Soup.Status.PARTIAL_CONTENT) {
                        out_stream = tmp_file.append_to(Gio.FileCreateFlags.NONE, cancellable);
                    } else {
                        out_stream = tmp_file.replace(null, false, Gio.FileCreateFlags.NONE, cancellable);
                    }

                    out_stream.splice_async(body, Gio.OutputStreamSpliceFlags.NONE,
                                            GLib.PRIORITY_DEFAULT, cancellable, task.catch_callback_errors((source, res) => {
                                                out_stream.splice_finish(res);
                                                out_stream.close(cancellable);
                                                finish_download(keep_new_file);
                                            }));
                }
            }));
        });

        return task;
    },

    download_file_finish: function (task) {
        return task.finish();
    },
});

// Returns items that are in A but not in B.
function subtract_set (A, B, key) {
    let result = [];
    let mapped_A = A.map(key);
    let mapped_B = B.map(key);

    for (let i = 0; i < mapped_A.length; i++) {
        let item = mapped_A[i];
        if (mapped_B.indexOf(item) === -1)
            result.push(A[i]);
    }
    return result;
}

function load_manifest (file, cancellable) {
    try {
        let [success, data] = file.load_contents(cancellable);
        return JSON.parse(data);
    } catch (e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
        return null;
    }
}

const SubscriptionDownloader = new Lang.Class({
    Name: 'SubscriptionDownloader',

    _init: function () {
        this._file_downloader = new FileDownloader();

        this._subscriptions_dir = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), 'com.endlessm.subscriptions']));
        Utils.ensure_directory(this._subscriptions_dir);

        this._load_from_config();
    },

    _load_from_config: function () {
        let config_env = GLib.getenv('EKN_UPDATER_CONFIG');
        if (config_env === null)
            config_env = Config.PKGDATADIR + '/downloader.ini';

        let kf = GLib.KeyFile.new();
        kf.load_from_file(config_env, GLib.KeyFileFlags.NONE);
        this._server_host = kf.get_string("Repository", "ServerUrl");
    },

    _download_new_shards: function (directory, old_manifest, new_manifest, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        let old_shards = old_manifest.shards;
        // Verify that client shards actually appear on disk.
        old_shards = old_shards.filter((shard) => {
            let shard_file = directory.get_child(shard.path);
            return shard_file.query_exists(cancellable);
        });

        let new_shards = new_manifest.shards;

        // Download any shards that are not on the client.

        // XXX: We're currently indexing by filename, assuming it will be good enough.
        // Will it be, or should we also store e.g. ETag?
        let to_download = subtract_set(new_shards, old_shards, (shard) => shard.path);

        AsyncTask.all(this, (add_task) => {
            to_download.forEach((shard) => {
                let shard_file = directory.get_child(shard.path);
                let shard_uri = Soup.URI.new(shard.download_uri);
                let shard_csum = shard.sha256_csum;

                let request = new FileDownloadRequest(shard_uri, shard_file, shard_csum);
                add_task((cancellable, callback) => this._file_downloader.download_file(request, cancellable, callback),
                         (task) => this._file_downloader.download_file_finish(task));
            });
        }, cancellable, task.catch_callback_errors((source, download_shards_task) => {
            AsyncTask.all_finish(download_shards_task);

            task.return_value(true);
        }));

        return task;
    },

    _download_new_shards_finish: function (task) {
        return task.finish();
    },

    apply_update: function (subscription_id, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        task.catch_errors(() => {
            let directory = this._subscriptions_dir.get_child(subscription_id);
            if (!directory.query_exists(cancellable))
                return task.return_value(false);

            let new_manifest_file = directory.get_child('manifest.json.new');
            let new_manifest = load_manifest(new_manifest_file, cancellable);
            // If we don't have a new manifest, we don't have an update.
            if (new_manifest === null)
                return task.return_value(false);

            let manifest_file = directory.get_child('manifest.json');
            let old_manifest = load_manifest(manifest_file, cancellable);
            if (old_manifest === null)
                old_manifest = { shards: [] };

            // Make sure we have all shards before we can apply the update.
            let new_shards = new_manifest.shards;
            let have_all_shards = new_shards.every((shard) => {
                let shard_file = directory.get_child(shard.path);
                return shard_file.query_exists(cancellable);
            });

            if (!have_all_shards)
                return task.return_value(false);

            // OK, we're ready to go and can apply the update. Move our new manifest,
            // and then clean up the old shards.
            new_manifest_file.move(manifest_file, Gio.FileCopyFlags.OVERWRITE, cancellable, null);

            let old_shards = old_manifest.shards;
            let new_shards = new_manifest.shards;
            let to_delete = subtract_set(old_shards, new_shards, (shard) => shard.path);

            to_delete.forEach((shard) => {
                let shard_file = directory.get_child(shard.path);
                shard_file.delete(cancellable);
            });

            return task.return_value(true);
        });

        return task;
    },

    apply_update_finish: function (task) {
        return task.finish();
    },

    fetch_update: function (subscription_id, cancellable, callback) {
        let task = new AsyncTask.AsyncTask(this, cancellable, callback);

        task.catch_errors(() => {
            let directory = this._subscriptions_dir.get_child(subscription_id);
            Utils.ensure_directory(directory);

            let manifest_file = directory.get_child('manifest.json');

            let old_manifest = load_manifest(manifest_file, cancellable);
            if (old_manifest === null)
                old_manifest = { shards: [] };

            let new_manifest_file = directory.get_child('manifest.json.new');
            let uri = Format.vprintf('%s/v1/%s/manifest.json', [this._server_host, subscription_id]);
            let manifest_uri = Soup.URI.new(uri);

            let request = new FileDownloadRequest(manifest_uri, new_manifest_file);
            this._file_downloader.download_file(request, cancellable, task.catch_callback_errors((source, download_task) => {
                try {
                    this._file_downloader.download_file_finish(download_task);
                } catch(e if e.matches(Gio.ResolverError, Gio.ResolverError.NOT_FOUND)) {
                    return task.return_value(false);
                }

                let new_manifest = load_manifest(new_manifest_file, cancellable);
                this._download_new_shards(directory, old_manifest, new_manifest, cancellable, task.catch_callback_errors((source, download_task) => {
                    this._download_new_shards_finish(download_task);
                    task.return_value(true);
                }));
            }));
        });

        return task;
    },

    fetch_update_finish: function (task) {
        return task.finish();
    },
});

const DownloaderIface = '\
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> \
  <interface name="com.endlessm.EknDownloader"> \
    <method name = "ApplyUpdate"> \
      <arg type="s" name="subscription_id" direction="in" /> \
      <arg type="b" name="applied_update" direction="out" /> \
    </method> \
    <method name = "FetchUpdate"> \
      <arg type="s" name="subscription_id" direction="in" /> \
    </method> \
  </interface> \
</node>';
const DownloaderIfaceInfo = Gio.DBusInterfaceInfo.new_for_xml(DownloaderIface);

function return_to_dbus (invocation, finish) {
    try {
        let value = finish();
        if (value === undefined)
            value = new GLib.Variant('()', []);
        invocation.return_value(value);
    } catch (e if e instanceof GLib.Error) {
        logError(e, "Exception in DBus call");
        invocation.return_gerror(e);
    } catch (e) {
        let name = e.name;
        if (name.indexOf('.') === -1) {
            // likely to be a normal JS error
            name = 'org.gnome.gjs.JSError.' + name;
        }
        logError(e, "Exception in DBus call");
        invocation.return_dbus_error(name, e.message);
    }
}

const DownloaderService = new Lang.Class({
    Name: 'DownloaderService',

    _init: function (app) {
        this._app = app;
        this._skeleton = Gio.DBusExportedObject.wrapJSObject(DownloaderIfaceInfo, this);
        this._downloader = new SubscriptionDownloader();
    },

    register: function (connection, path) {
        this._skeleton.export(connection, path);
    },

    unregister: function (connection, path) {
        this._skeleton.unexport(connection, path);
    },

    ApplyUpdateAsync: function (params, invocation) {
        let [subscription_id] = params;
        this._app.hold();
        return this._downloader.apply_update(subscription_id, null, (source, task) => {
            return_to_dbus(invocation, () => {
                let res = this._downloader.apply_update_finish(task);
                return new GLib.Variant('(b)', [res]);
            });
            this._app.release();
        });
    },

    FetchUpdateAsync: function (params, invocation) {
        let [subscription_id] = params;
        this._app.hold();
        return this._downloader.fetch_update(subscription_id, null, (source, task) => {
            return_to_dbus(invocation, () => {
                this._downloader.fetch_update_finish(task);
            });
            this._app.release();
        });
    },
});

const Application = new Lang.Class({
    Name: 'EknDownloaderApplication',
    Extends: Gio.Application,

    _init: function () {
        this.parent({ application_id: 'com.endlessm.EknDownloader',
                      flags: Gio.ApplicationFlags.IS_SERVICE,
                      inactivity_timeout: 12000 });

        this._service = new DownloaderService(this);
    },

    vfunc_dbus_register: function (connection, path) {
        this.parent(connection, path);
        this._service.register(connection, path);
        return true;
    },

    vfunc_dbus_unregister: function (connection, path) {
        this.parent(connection, path);
        this._service.unregister(connection, path);
    },
});
<|endoftext|>


<commit_before>var Botkit = require('botkit')
  , users = require('./model/users')
  , controller;

controller = Botkit.slackbot({
  debug: true
});

// connect the bot to a stream of messages
controller.spawn({
  token: process.env.SLACK_API_TOKEN
}).startRTM();

// user management:
// join
controller.hears('join', 'direct_mention', function(bot, message) {
  bot.api.users.info({user: message.user}, function(err, user) {
    if(err) {
      return bot.reply(message, 'Oops! I wasn\'t able to add you right now, maybe try again in a minute');
    }
    users.add(user);
    bot.reply(message, 'You\'re on the roster ' + user.name + ' :thumbsup:');
  });

});
// leave
// kick/remove

// standup admin:
// start
// stop (have auto-stop when reach last user)
// list participants
// skip (current user)

// standup report
// notify next user
// conversation:
// * what did you do yesterday?
// * what are you doing today?
// * anything in your way?<commit_msg>debug mode off<commit_after>var Botkit = require('botkit')
  , users = require('./model/users')
  , controller;

controller = Botkit.slackbot({
  debug: false
});

// connect the bot to a stream of messages
controller.spawn({
  token: process.env.SLACK_API_TOKEN
}).startRTM();

// user management:
// join
controller.hears('join', 'direct_mention', function(bot, message) {
  bot.api.users.info({user: message.user}, function(err, user) {
    if(err) {
      return bot.reply(message, 'Oops! I wasn\'t able to add you right now, maybe try again in a minute');
    }
    users.add(user);
    bot.reply(message, 'You\'re on the roster ' + user.name + ' :thumbsup:');
  });

});
// leave
// kick/remove

// standup admin:
// start
// stop (have auto-stop when reach last user)
// list participants
// skip (current user)

// standup report
// notify next user
// conversation:
// * what did you do yesterday?
// * what are you doing today?
// * anything in your way?<|endoftext|>


<commit_before>/* global SlideShow */
(function ($, SlideShow) {
	"use strict";
	/**
	 * Button and key controls for the slideshow
	 *
	 * @param {Object} slideshow
	 * @param {*} container
	 * @param {Object} zoomablePreview
	 * @param {number} interval
	 * @constructor
	 */
	var Controls = function (slideshow, container, zoomablePreview, interval) {
		this.slideshow = slideshow;
		this.container = container;
		this.zoomablePreview = zoomablePreview;
		this.progressBar = container.find('.progress');
		this.interval = interval || 5000;

	};

	Controls.prototype = {
		current: 0,
		errorLoadingImage: false,
		playTimeout: 0,
		playing: false,
		active: false,

		/**
		 * Initialises the controls
		 */
		init: function () {
			var makeCallBack = function (handler) {
				return function (evt) {
					if (!this.active) {
						return;
					}
					evt.stopPropagation();
					handler.call(this);
				}.bind(this);
			}.bind(this);

			this._buttonSetup(makeCallBack);
			this._specialButtonSetup(makeCallBack);
			this._keyCodeSetup(makeCallBack);
		},

		/**
		 * Updates the controls
		 *
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
		 * @param {bool} autoPlay
		 */
		update: function (images, autoPlay) {
			this.images = images;
			this.active = true;
			this.container.find('.pause').hide();

			// Hide prev/next and play buttons when we only have one pic
			this.container.find('.next, .previous, .play').toggle(this.images.length > 1);

			// Hide the toggle background button until we have something to show
			this.container.find('.changeBackground').hide();

			if (autoPlay) {
				this._play();
			}
		},

		/**
		 * Initialises local variables when the show starts
		 *
		 * @param {number} currentImageId
		 */
		show: function (currentImageId) {
			this.current = currentImageId;
			this.errorLoadingImage = false;
			if (this.playing) {
				this._setTimeout();
			}
		},

		/**
		 * Stops the timed slideshow
		 */
		stop: function () {
			this.playing = false;
			this.slideshow.stop();
			this.zoomablePreview.stop();

			this._clearTimeout();
			this.container.hide();
			this.active = false;
		},

		/**
		 * Updates the private variables in case of problems loading an image
		 *
		 * @param {Array} images
		 * @param {bool} errorLoadingImage
		 */
		updateControls: function (images, errorLoadingImage) {
			this.images = images;
			this.errorLoadingImage = errorLoadingImage;
		},

		/**
		 * Sets up the button based navigation
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_buttonSetup: function (makeCallBack) {
			this.container.children('.next').click(makeCallBack(this._next));
			this.container.children('.previous').click(makeCallBack(this._previous));
			this.container.children('.exit').click(makeCallBack(this._exit));
			this.container.children('.pause').click(makeCallBack(this._pause));
			this.container.children('.play').click(makeCallBack(this._play));
		},

		/**
		 * Sets up additional buttons
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_specialButtonSetup: function (makeCallBack) {
			this.container.children('.downloadImage').click(makeCallBack(this._getImageDownload));
			this.container.children('.changeBackground').click(makeCallBack(this._toggleBackground));
		},

		/**
		 * Sets up the key based controls
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_keyCodeSetup: function (makeCallBack) {
			$(document).keyup(function (evt) {
				var escKey = 27;
				var leftKey = 37;
				var rightKey = 39;
				var spaceKey = 32;
				var fKey = 70;
				var zoomOutKeys = [48, 96, 79, 40]; // zero, o or down key
				var zoomInKeys = [57, 105, 73, 38]; // 9, i or up key
				if (evt.keyCode === escKey) {
					makeCallBack(this._stop)(evt);
				} else if (evt.keyCode === leftKey) {
					makeCallBack(this._previous)(evt);
				} else if (evt.keyCode === rightKey) {
					makeCallBack(this._next)(evt);
				} else if (evt.keyCode === spaceKey) {
					makeCallBack(this._play)(evt);
				} else if (evt.keyCode === fKey) {
					makeCallBack(this._fullScreenToggle)(evt);
				} else if (this._hasKeyBeenPressed(evt, zoomOutKeys)) {
					makeCallBack(this._zoomToOriginal)(evt);
				} else if (this._hasKeyBeenPressed(evt, zoomInKeys)) {
					makeCallBack(this._zoomToFit)(evt);
				}
			}.bind(this));
		},

		/**
		 * Determines if a key has been pressed by comparing the event and the key
		 *
		 * @param evt
		 * @param {Array} keys
		 *
		 * @returns {boolean}
		 * @private
		 */
		_hasKeyBeenPressed: function (evt, keys) {
			var i, keysLength = keys.length;
			for (i = 0; i < keysLength; i++) {
				if (evt.keyCode === keys[i]) {
					return true;
				}
			}
			return false;
		},

		/**
		 * Starts the slideshow timer
		 *
		 * @private
		 */
		_setTimeout: function () {
			this._clearTimeout();
			this.playTimeout = setTimeout(this._next.bind(this), this.interval);
			this.progressBar.stop();
			this.progressBar.css('height', '6px');
			this.progressBar.animate({'height': '26px'}, this.interval, 'linear');
		},

		/**
		 * Stops the slideshow timer
		 *
		 * @private
		 */
		_clearTimeout: function () {
			if (this.playTimeout) {
				clearTimeout(this.playTimeout);
			}
			this.progressBar.stop();
			this.progressBar.css('height', '6px');
			this.playTimeout = 0;
		},

		/**
		 * Starts the timed slideshow
		 *
		 * @private
		 */
		_play: function () {
			this.playing = true;
			this._playPauseButtonToggle();
			this._setTimeout();
		},

		/**
		 * Pauses the timed slideshow
		 *
		 * @private
		 */
		_pause: function () {
			this.playing = false;
			this._playPauseButtonToggle();
			this._clearTimeout();
		},

		/**
		 * Shows the play or pause button depending on circumstances
		 *
		 * @private
		 */
		_playPauseButtonToggle: function () {
			this.container.find('.play, .pause').toggle();
		},

		/**
		 * Shows the next slide
		 *
		 * @private
		 */
		_next: function () {
			this.slideshow.hideErrorNotification();
			this.zoomablePreview.reset();

			if (this.errorLoadingImage) {
				this.current -= 1;
			}
			this.current = (this.current + 1) % this.images.length;
			var next = (this.current + 1) % this.images.length;
			this._updateSlideshow(next);
		},

		/**
		 * Shows the previous slide
		 *
		 * @private
		 */
		_previous: function () {
			this.slideshow.hideErrorNotification();
			this.zoomablePreview.reset();

			this.current = (this.current - 1 + this.images.length) % this.images.length;
			var previous = (this.current - 1 + this.images.length) % this.images.length;
			this._updateSlideshow(previous);
		},

		/**
		 * Shows a new image in the slideshow and preloads the next in the list
		 *
		 * @param {number} imageId
		 * @private
		 */
		_updateSlideshow: function (imageId) {
			this.slideshow.show(this.current).then(function () {
				// Preloads the next image in the list
				this.slideshow.loadImage(this.images[imageId]);
			}.bind(this));
		},

		/**
		 * Exits the slideshow by going back in history
		 *
		 * @private
		 */
		_exit: function () {

			// Only modern browsers can manipulate history
			if (history && history.replaceState) {
				// We simulate a click on the back button in order to be consistent
				window.history.back();
			} else{
				// For ancient browsers supported in core
				this.stop();
			}
		},

		/**
		 * Launches fullscreen mode if the browser supports it
		 *
		 * @private
		 */
		_fullScreenToggle: function () {
			this.zoomablePreview.fullScreenToggle();
		},

		/**
		 * Resizes the image to its original size
		 *
		 * @private
		 */
		_zoomToOriginal: function () {
			this.zoomablePreview.zoomToOriginal();
		},

		/**
		 * Fits the image in the browser window
		 *
		 * @private
		 */
		_zoomToFit: function () {
			this.zoomablePreview.zoomToFit();
		},

		/**
		 * Sends the current image as a download
		 *
		 * @returns {boolean}
		 * @private
		 */
		_getImageDownload: function () {
			var downloadUrl = this.images[this.current].downloadUrl;

			return this.slideshow.getImageDownload(downloadUrl);
		},

		/**
		 * Changes the colour of the background of the image
		 *
		 * @private
		 */
		_toggleBackground: function () {
			this.slideshow.toggleBackground();
		}

	};

	SlideShow.Controls = Controls;
})(jQuery, SlideShow);
<commit_msg>Regression: escape key wasn't exiting the slideshow anymore Fixes #251<commit_after>/* global SlideShow */
(function ($, SlideShow) {
	"use strict";
	/**
	 * Button and key controls for the slideshow
	 *
	 * @param {Object} slideshow
	 * @param {*} container
	 * @param {Object} zoomablePreview
	 * @param {number} interval
	 * @constructor
	 */
	var Controls = function (slideshow, container, zoomablePreview, interval) {
		this.slideshow = slideshow;
		this.container = container;
		this.zoomablePreview = zoomablePreview;
		this.progressBar = container.find('.progress');
		this.interval = interval || 5000;

	};

	Controls.prototype = {
		current: 0,
		errorLoadingImage: false,
		playTimeout: 0,
		playing: false,
		active: false,

		/**
		 * Initialises the controls
		 */
		init: function () {
			var makeCallBack = function (handler) {
				return function (evt) {
					if (!this.active) {
						return;
					}
					evt.stopPropagation();
					handler.call(this);
				}.bind(this);
			}.bind(this);

			this._buttonSetup(makeCallBack);
			this._specialButtonSetup(makeCallBack);
			this._keyCodeSetup(makeCallBack);
		},

		/**
		 * Updates the controls
		 *
		 * @param {{name:string, url: string, path: string, fallBack: string}[]} images
		 * @param {bool} autoPlay
		 */
		update: function (images, autoPlay) {
			this.images = images;
			this.active = true;
			this.container.find('.pause').hide();

			// Hide prev/next and play buttons when we only have one pic
			this.container.find('.next, .previous, .play').toggle(this.images.length > 1);

			// Hide the toggle background button until we have something to show
			this.container.find('.changeBackground').hide();

			if (autoPlay) {
				this._play();
			}
		},

		/**
		 * Initialises local variables when the show starts
		 *
		 * @param {number} currentImageId
		 */
		show: function (currentImageId) {
			this.current = currentImageId;
			this.errorLoadingImage = false;
			if (this.playing) {
				this._setTimeout();
			}
		},

		/**
		 * Stops the timed slideshow
		 */
		stop: function () {
			this.playing = false;
			this.slideshow.stop();
			this.zoomablePreview.stop();

			this._clearTimeout();
			this.container.hide();
			this.active = false;
		},

		/**
		 * Updates the private variables in case of problems loading an image
		 *
		 * @param {Array} images
		 * @param {bool} errorLoadingImage
		 */
		updateControls: function (images, errorLoadingImage) {
			this.images = images;
			this.errorLoadingImage = errorLoadingImage;
		},

		/**
		 * Sets up the button based navigation
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_buttonSetup: function (makeCallBack) {
			this.container.children('.next').click(makeCallBack(this._next));
			this.container.children('.previous').click(makeCallBack(this._previous));
			this.container.children('.exit').click(makeCallBack(this._exit));
			this.container.children('.pause').click(makeCallBack(this._pause));
			this.container.children('.play').click(makeCallBack(this._play));
		},

		/**
		 * Sets up additional buttons
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_specialButtonSetup: function (makeCallBack) {
			this.container.children('.downloadImage').click(makeCallBack(this._getImageDownload));
			this.container.children('.changeBackground').click(makeCallBack(this._toggleBackground));
		},

		/**
		 * Sets up the key based controls
		 *
		 * @param {Function} makeCallBack
		 * @private
		 */
		_keyCodeSetup: function (makeCallBack) {
			$(document).keyup(function (evt) {
				var escKey = 27;
				var leftKey = 37;
				var rightKey = 39;
				var spaceKey = 32;
				var fKey = 70;
				var zoomOutKeys = [48, 96, 79, 40]; // zero, o or down key
				var zoomInKeys = [57, 105, 73, 38]; // 9, i or up key
				if (evt.keyCode === escKey) {
					makeCallBack(this._exit)(evt);
				} else if (evt.keyCode === leftKey) {
					makeCallBack(this._previous)(evt);
				} else if (evt.keyCode === rightKey) {
					makeCallBack(this._next)(evt);
				} else if (evt.keyCode === spaceKey) {
					makeCallBack(this._play)(evt);
				} else if (evt.keyCode === fKey) {
					makeCallBack(this._fullScreenToggle)(evt);
				} else if (this._hasKeyBeenPressed(evt, zoomOutKeys)) {
					makeCallBack(this._zoomToOriginal)(evt);
				} else if (this._hasKeyBeenPressed(evt, zoomInKeys)) {
					makeCallBack(this._zoomToFit)(evt);
				}
			}.bind(this));
		},

		/**
		 * Determines if a key has been pressed by comparing the event and the key
		 *
		 * @param evt
		 * @param {Array} keys
		 *
		 * @returns {boolean}
		 * @private
		 */
		_hasKeyBeenPressed: function (evt, keys) {
			var i, keysLength = keys.length;
			for (i = 0; i < keysLength; i++) {
				if (evt.keyCode === keys[i]) {
					return true;
				}
			}
			return false;
		},

		/**
		 * Starts the slideshow timer
		 *
		 * @private
		 */
		_setTimeout: function () {
			this._clearTimeout();
			this.playTimeout = setTimeout(this._next.bind(this), this.interval);
			this.progressBar.stop();
			this.progressBar.css('height', '6px');
			this.progressBar.animate({'height': '26px'}, this.interval, 'linear');
		},

		/**
		 * Stops the slideshow timer
		 *
		 * @private
		 */
		_clearTimeout: function () {
			if (this.playTimeout) {
				clearTimeout(this.playTimeout);
			}
			this.progressBar.stop();
			this.progressBar.css('height', '6px');
			this.playTimeout = 0;
		},

		/**
		 * Starts the timed slideshow
		 *
		 * @private
		 */
		_play: function () {
			this.playing = true;
			this._playPauseButtonToggle();
			this._setTimeout();
		},

		/**
		 * Pauses the timed slideshow
		 *
		 * @private
		 */
		_pause: function () {
			this.playing = false;
			this._playPauseButtonToggle();
			this._clearTimeout();
		},

		/**
		 * Shows the play or pause button depending on circumstances
		 *
		 * @private
		 */
		_playPauseButtonToggle: function () {
			this.container.find('.play, .pause').toggle();
		},

		/**
		 * Shows the next slide
		 *
		 * @private
		 */
		_next: function () {
			this.slideshow.hideErrorNotification();
			this.zoomablePreview.reset();

			if (this.errorLoadingImage) {
				this.current -= 1;
			}
			this.current = (this.current + 1) % this.images.length;
			var next = (this.current + 1) % this.images.length;
			this._updateSlideshow(next);
		},

		/**
		 * Shows the previous slide
		 *
		 * @private
		 */
		_previous: function () {
			this.slideshow.hideErrorNotification();
			this.zoomablePreview.reset();

			this.current = (this.current - 1 + this.images.length) % this.images.length;
			var previous = (this.current - 1 + this.images.length) % this.images.length;
			this._updateSlideshow(previous);
		},

		/**
		 * Shows a new image in the slideshow and preloads the next in the list
		 *
		 * @param {number} imageId
		 * @private
		 */
		_updateSlideshow: function (imageId) {
			this.slideshow.show(this.current).then(function () {
				// Preloads the next image in the list
				this.slideshow.loadImage(this.images[imageId]);
			}.bind(this));
		},

		/**
		 * Exits the slideshow by going back in history
		 *
		 * @private
		 */
		_exit: function () {

			// Only modern browsers can manipulate history
			if (history && history.replaceState) {
				// We simulate a click on the back button in order to be consistent
				window.history.back();
			} else{
				// For ancient browsers supported in core
				this.stop();
			}
		},

		/**
		 * Launches fullscreen mode if the browser supports it
		 *
		 * @private
		 */
		_fullScreenToggle: function () {
			this.zoomablePreview.fullScreenToggle();
		},

		/**
		 * Resizes the image to its original size
		 *
		 * @private
		 */
		_zoomToOriginal: function () {
			this.zoomablePreview.zoomToOriginal();
		},

		/**
		 * Fits the image in the browser window
		 *
		 * @private
		 */
		_zoomToFit: function () {
			this.zoomablePreview.zoomToFit();
		},

		/**
		 * Sends the current image as a download
		 *
		 * @returns {boolean}
		 * @private
		 */
		_getImageDownload: function () {
			var downloadUrl = this.images[this.current].downloadUrl;

			return this.slideshow.getImageDownload(downloadUrl);
		},

		/**
		 * Changes the colour of the background of the image
		 *
		 * @private
		 */
		_toggleBackground: function () {
			this.slideshow.toggleBackground();
		}

	};

	SlideShow.Controls = Controls;
})(jQuery, SlideShow);
<|endoftext|>


<commit_before>'use strict';

import PureComponent from '../PureComponent';

var React = require('react');
import XAutosuggest from './XAutosuggest';
import Input from 'react-toolbox/lib/input';
import {Observable, Scheduler} from '../rx';

var {matchPartialField, sparseDataMatchPartialField, refGene} = require('../xenaQuery');
var _ = require('../underscore_ext');
var {rxEvents} = require('../react-utils');
require('./GeneSuggest.css'); // react-autosuggest, global styles
var styles = require('./GeneSuggest.module.css'); // react-toolbox, module styles
var limit = 8;

// Return the start and end indices of the word in 'value'
// under the cursor position.
function currentWordPosition(value, position) {
	var li = value.slice(0, position).lastIndexOf(' '),
		i = li === -1 ? 0 : li + 1,
		lj = value.slice(position).indexOf(' '),
		j = lj === -1 ? value.length : position + lj;
	return [i, j];
}

// Return the word in 'value' under the cursor position
function currentWord(value, position) {
	var [i, j] = currentWordPosition(value, position);
	return value.slice(i, j);
}

var renderInputComponent = ({ref, onChange, label, error, ...props}) => (
	<Input
		theme={styles}
		error={_.isString(error) ? error : null}
		spellCheck={false}
		innerRef={el => ref(el && el.inputNode)}
		onChange={(value, ev) => onChange(ev)}
		label={label || 'Add Gene or Position'}
		{...props} >
		error
	</Input>
);

var empty = Observable.of([], Scheduler.asap);

// host and name are for gene lookup.
// dataset is for probe lookup
var fetchSuggestions = (assembly, dataset, value) =>
	Observable.zip(
		assembly ? sparseDataMatchPartialField(assembly.host, 'name2', assembly.name, value, limit).catch(() => empty) :
		empty,
		dataset ? matchPartialField(dataset, value, limit).catch(() => empty) :
		empty,
		(genes, probes) => _.uniq(genes.concat(probes)).sort());

// Currently we only match against refGene hg38 genes. We could, instead, match
// on specific datasets (probemap, mutation, segmented, refGene), but that will
// require some more work to dispatch the query for each type.
class GeneSuggest extends PureComponent {
	state = {suggestions: []};

	componentWillMount() {
		var assembly = refGene[this.props.assembly];
		var events = rxEvents(this, 'change');
		this.change = events.change
			.distinctUntilChanged(_.isEqual)
			.debounceTime(200)
			.switchMap(value => fetchSuggestions(assembly, this.props.dataset, value))
			.subscribe(matches => this.setState({suggestions: matches}));
	}

	componentWillUnmount() {
		this.change.unsubscribe();
	}

	onSuggestionsFetchRequested = ({value}) => {
		var position = this.input.selectionStart,
			word = currentWord(value, position);

		if (word !== '') {
			this.on.change(word);
		}
	};

	shouldRenderSuggestions = (value) => {
		var position = this.input.selectionStart,
			word = currentWord(value, position);
		return word.length > 0;
	};

	onSuggestionsClearRequested = () => {
		this.setState({suggestions: []});
	};

	onChange = (ev, {newValue, method}) => {
		// Don't update the value for 'up' and 'down' keys. If we do update
		// the value, it gives us an in-place view of the suggestion (pasting
		// the value into the input field), but the drawback is that it moves
		// the cursor to the end of the line. This messes up multi-word input.
		// We could try to preserve the cursor position, perhaps by passing a
		// custom input renderer. But for now, just don't update the value for
		// these events.
		if (method !== 'up' && method !== 'down') {
			this.props.onChange(newValue);
		}
	};

	getSuggestionValue = (suggestion) => {
		var position = this.input.selectionStart,
			value = this.input.value,
			[i, j] = currentWordPosition(value, position);

		// splice the suggestion into the current word
		return value.slice(0, i) + suggestion + value.slice(j);
	};

	setInput = (input) => {
		var {inputRef} = this.props;
		this.input = input;
		if (inputRef) {
			inputRef(this.input);
		}
	};

	render() {
		var {onChange} = this,
			{onKeyDown, value = '', label, error} = this.props,
			{suggestions} = this.state;

		return (
			<XAutosuggest
				inputRef={this.setInput}
				suggestions={suggestions}
				onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
				onSuggestionsClearRequested={this.onSuggestionsClearRequested}
				getSuggestionValue={this.getSuggestionValue}
				shouldRenderSuggestions={this.shouldRenderSuggestions}
				renderSuggestion={v => {v}}
				renderInputComponent={renderInputComponent}
				inputProps={{value, label, error, onKeyDown, onChange}}/>);
	}
}

module.exports = GeneSuggest;
<commit_msg>Don't cache assembly during gene suggest<commit_after>'use strict';

import PureComponent from '../PureComponent';

var React = require('react');
import XAutosuggest from './XAutosuggest';
import Input from 'react-toolbox/lib/input';
import {Observable, Scheduler} from '../rx';

var {matchPartialField, sparseDataMatchPartialField, refGene} = require('../xenaQuery');
var _ = require('../underscore_ext');
var {rxEvents} = require('../react-utils');
require('./GeneSuggest.css'); // react-autosuggest, global styles
var styles = require('./GeneSuggest.module.css'); // react-toolbox, module styles
var limit = 8;

// Return the start and end indices of the word in 'value'
// under the cursor position.
function currentWordPosition(value, position) {
	var li = value.slice(0, position).lastIndexOf(' '),
		i = li === -1 ? 0 : li + 1,
		lj = value.slice(position).indexOf(' '),
		j = lj === -1 ? value.length : position + lj;
	return [i, j];
}

// Return the word in 'value' under the cursor position
function currentWord(value, position) {
	var [i, j] = currentWordPosition(value, position);
	return value.slice(i, j);
}

var renderInputComponent = ({ref, onChange, label, error, ...props}) => (
	<Input
		theme={styles}
		error={_.isString(error) ? error : null}
		spellCheck={false}
		innerRef={el => ref(el && el.inputNode)}
		onChange={(value, ev) => onChange(ev)}
		label={label || 'Add Gene or Position'}
		{...props} >
		error
	</Input>
);

var empty = Observable.of([], Scheduler.asap);

// host and name are for gene lookup.
// dataset is for probe lookup
var fetchSuggestions = (assembly, dataset, value) =>
	Observable.zip(
		assembly ? sparseDataMatchPartialField(assembly.host, 'name2', assembly.name, value, limit).catch(() => empty) :
		empty,
		dataset ? matchPartialField(dataset, value, limit).catch(() => empty) :
		empty,
		(genes, probes) => _.uniq(genes.concat(probes)).sort());

// Currently we only match against refGene hg38 genes. We could, instead, match
// on specific datasets (probemap, mutation, segmented, refGene), but that will
// require some more work to dispatch the query for each type.
class GeneSuggest extends PureComponent {
	state = {suggestions: []};

	componentWillMount() {
		var events = rxEvents(this, 'change');
		this.change = events.change
			.distinctUntilChanged(_.isEqual)
			.debounceTime(200)
			.switchMap(value => fetchSuggestions(refGene[this.props.assembly], this.props.dataset, value))
			.subscribe(matches => this.setState({suggestions: matches}));
	}

	componentWillUnmount() {
		this.change.unsubscribe();
	}

	onSuggestionsFetchRequested = ({value}) => {
		var position = this.input.selectionStart,
			word = currentWord(value, position);

		if (word !== '') {
			this.on.change(word);
		}
	};

	shouldRenderSuggestions = (value) => {
		var position = this.input.selectionStart,
			word = currentWord(value, position);
		return word.length > 0;
	};

	onSuggestionsClearRequested = () => {
		this.setState({suggestions: []});
	};

	onChange = (ev, {newValue, method}) => {
		// Don't update the value for 'up' and 'down' keys. If we do update
		// the value, it gives us an in-place view of the suggestion (pasting
		// the value into the input field), but the drawback is that it moves
		// the cursor to the end of the line. This messes up multi-word input.
		// We could try to preserve the cursor position, perhaps by passing a
		// custom input renderer. But for now, just don't update the value for
		// these events.
		if (method !== 'up' && method !== 'down') {
			this.props.onChange(newValue);
		}
	};

	getSuggestionValue = (suggestion) => {
		var position = this.input.selectionStart,
			value = this.input.value,
			[i, j] = currentWordPosition(value, position);

		// splice the suggestion into the current word
		return value.slice(0, i) + suggestion + value.slice(j);
	};

	setInput = (input) => {
		var {inputRef} = this.props;
		this.input = input;
		if (inputRef) {
			inputRef(this.input);
		}
	};

	render() {
		var {onChange} = this,
			{onKeyDown, value = '', label, error} = this.props,
			{suggestions} = this.state;

		return (
			<XAutosuggest
				inputRef={this.setInput}
				suggestions={suggestions}
				onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
				onSuggestionsClearRequested={this.onSuggestionsClearRequested}
				getSuggestionValue={this.getSuggestionValue}
				shouldRenderSuggestions={this.shouldRenderSuggestions}
				renderSuggestion={v => {v}}
				renderInputComponent={renderInputComponent}
				inputProps={{value, label, error, onKeyDown, onChange}}/>);
	}
}

module.exports = GeneSuggest;
<|endoftext|>