<commit_before>---
yuiCompress: !!bool false
---
(function($, window, document) {

    var $c = $('#footer'),
        $f = $c.find('#feedback'),
        $i = $c.find('#input'),
        $fi = $c.find('#fake-input'),
        $cursor = $c.find('#cursor'),
        $dummy = $('<div />', {id: 'dummy'}).appendTo('body'),
        cEnabled = false,
        cInput = '',
        cLastKeyWasTab = false,
        cMaxLen = 40,
        cToggleKey = 192, // this is ~
        cmds = {}, // defined later
        spaceWidth = 0, // defined later
        siteMap = <%= get_sitemap %>, // used for completion (ls/cd)
        siteMapIds = <%= get_sitemap_ids %>, // used for navigation

    /**
     * Useful to create alias functions.
     */
    alias = function(cmd, type) {
        return function() {
            if (type === 'completers') {
                return completers[cmd].apply(this, arguments);
            }
            return cmds[cmd].apply(this, arguments);
        }
    },

    /**
     * Given a command, return options what the command could be.
     */
    completeCmd = function(cmd) {
        var cmdSplit = cmd.split(/\s+/g);
        if (cmdSplit.length === 1) {
            return helpers.filterPrefixed(cmd, Object.keys(cmds));
        } else if (completers.hasOwnProperty(cmdSplit[0])) {
            cmd = cmdSplit.splice(0, 1);
            var matches = completers[cmd].apply(this, cmdSplit);
            matches.prefix = cmd;
            return matches;
        }
        return [];
    },

    /**
     * Encode HTML entities in the given string, and make every space a
     * non-breaking one. Useful when showing user input to user.
     *
     * \param str The string to encode.
     * \param plainStr Optional boolean, default \c false. When true, return
     *            a string with plain characters instead of HTML escape
     *            sequences. The string will be Unicode.
     */
    encode = function(str, plainStr) {
        if (typeof(str) !== 'string') {
            return '';
        }
        if (typeof(plainStr) !== 'boolean') {
            plainStr = false;
        }
        return str.replace(/[\u00A0-\u9999<>\& ]/gim, function(i) {
            if (i === ' ') {
                return (plainStr ? '\u00A0' : ' ');
            }
            return (plainStr ? i.charAt(0) : '&#' + i.charCodeAt(0) + ';');
        });
    },

    /**
     * Compute the width of the given string using a dummy element.
     */
    getTextWidth = function(str) {
        // use a cached dummy
        return $dummy.html(str).width();
    },

    /**
     * Triggered for \c keypress events on the window. Should toggle the
     * console status for the activation key.
     */
    onKeyDown = function(e) {
        // handle tab here, before it changes the focus
        // also respond to global toggle key events
        if (e.which === 9 || e.which === cToggleKey) {
            onKeyUp(e);
        }
    },

    /**
     * Handler for key press events in the console input. Should only do
     * something if the console is enabled.
     */
    onKeyUp = function(e) {
        if (cEnabled && e.which === cToggleKey) {
            return;
        }
        // see if we should handle this key press
        var handled = true,
            thisKeyIsTab = false;
        switch (e.which) {
            case cToggleKey: // is this to blur the console?
                cEnabled = !cEnabled;
                if (cEnabled) {
                    $fi.focus();
                } else {
                    $i.blur();
                }
                break;
            case 8: // backspace
                // we only update the input after the switch, that is all
                break;
            case 9: // tab
                thisKeyIsTab = true;
                if (e.type === 'keyup') {
                    // already handled in keydown
                    break;
                }
                var matches = completeCmd(cInput);
                if (matches.length === 1) {
                    setInput((matches.prefix ? matches.prefix + ' ' : '') +
                        matches[0] + ' ');
                } else if (matches.length > 1 && cLastKeyWasTab) {
                    setFeedback(matches);
                }
                break;
            case 13: // enter
                setFeedback(parseCmd(cInput));
                cmds.clear();
                break;
            default:
                handled = false;
        }
        // did we do anything? then mark event as handled
        if (handled) {
            e.preventDefault();
            e.stopPropagation();
            cLastKeyWasTab = thisKeyIsTab;
            // update text in input
            updateInput();
        }
        // always update cursor position, because we do not explicitly handle
        // home, end, arrow keys, et cetera
        updateCursorPosition();
    },

    /**
     * See if we know the given command and act on it.
     */
    parseCmd = function(cmd) {
        cmd = cmd.trim().toLowerCase().split(/\s+/g);
        if (cmd.length > 0) {
            if (cmds.hasOwnProperty(cmd[0])) {
                var cmdNm = cmd.splice(0, 1);
                return cmds[cmdNm[0]].apply(this, cmd);
            }
        }
        return 'desh: Unknown command "' + cmd + '".';
    },

    /**
     * Shorthand function to create a function that returns its argument.
     */
    s = function(str) {
        return function() {
            return str;
        }
    },

    /**
     * Set the contents of the feedback.
     */
    setFeedback = function(str) {
        if (typeof(str) !== 'string') {
            if ((Array.isArray && Array.isArray(str)) ||
                    str instanceof Array) {
                str = str.join('    ');
            } else {
                str = '';
            }
        }
        $f.html(encode(str));
        $c.toggleClass('tall', (str != ''));
    },

    /**
     * Set the contents of the input.
     *
     * Both the hidden and the fake one. Does need a call to updateInput to
     * see the effect though.
     */
    setInput = function(str) {
        cInput = str;
        $i.val(str);
    },

    /**
     * Update the position of the cursor so it matches the position of the
     * cursor in the hidden input element.
     */
    updateCursorPosition = function() {
        // position cursor absolute iff console is enabled
        $cursor.toggleClass('dynamic', cEnabled);
        if (cEnabled) {
            // determine cursor position
            var input = $i.get(0),
                selS = input.selectionStart,
                selE = input.selectionEnd,
                lastChar = cInput.substr(selE, 1);
            // selection is not supported, only cursor movement
            if (selE !== selS) {
                input.selectionStart = selE;
            }
            // get the length of the string up to the cursor
            $cursor.css('margin-left', getTextWidth(encode('>' + cInput.substr(0, selE))));
            if (lastChar === '') {
                $cursor.html(' ').css('width', '.7em');
            } else if (lastChar === ' ') {
                $cursor.html(' ').css('width', spaceWidth);
            } else {
                $cursor.text(lastChar).css('width', '');
            }
        } else {
            $cursor.html(' ').css({
                marginLeft: '',
                width: ''
            });
        }
    },

    /**
     * Update input text from the global \c cInput variable.
     */
    updateInput = function() {
        $fi.html(encode(cInput));
    },

    /**
     * Command definitions. Hey. Cheater! Reading the source and all that.
     * Well. Have fun, hacker :-)
     */
    cmds = {
        'about': alias('help'),
        'about-me': s('This is desh, the Denvelop Shell, programmed by Thom.'),
        'cd': function(path) {
            if (typeof(path) === 'undefined') {
                path = '/';
            }
            // make path absolute
            path = helpers.dirResolve(location.pathname, path);
            // if the path can be found in the sitemap, go there, otherwise error
            if (siteMap.indexOf(path) === -1) {
                return 'desh: Invalid path.';
            }
            // navigate to page
            $(window).trigger('denvelop-navigate', [path, siteMapIds[path]]);
        },
        'clear': function() { setInput(''); updateInput(); },
        'help': s('desh: This is a tiny console for geeks. No room for ' +
                    'further help, sorry.'),
        'ls': function(path) {
            var cwd = location.pathname,
                items = [], i, l, item, itemIndex;
            if (typeof(path) === 'string') {
                cwd = helpers.dirResolve(cwd, path);
                if (cwd === null) {
                    return 'desh: Invalid path.';
                }
            }
            // consider all items the user may mean
            for (i = 0, l = siteMap.length; i < l; ++i) {
                if (siteMap[i].startsWith(cwd)) {
                    item = siteMap[i].substr(cwd.length);
                    // the item itself is referred to with a single dot
                    if (item === '') {
                        item = '.';
                    }
                    // we do not list directories recursively
                    itemIndex = item.indexOf('/');
                    if (itemIndex < 0 || itemIndex === item.length - 1) {
                        items.push(item);
                    }
                }
            }
            // if may be possible to move up a directory
            if (cwd !== '/' && items.indexOf('.') !== -1) {
                items.push('..');
            }
            // present items in sorted order
            items.sort();
            return items;
        },
        'theme': function(name) {
            $(window).one('denvelop-theme-switched', function(e, switched, newTheme) {
                if (!switched) {
                    if (typeof(name) === 'undefined') {
                        setFeedback('desh: Current theme is "' + newTheme + '".');
                    } else {
                        setFeedback('desh: Invalid theme name.');
                    }
                }
            });
            $(window).trigger('denvelop-theme-switch', [name]);
        }
    },

    /**
     * Tab completion functions per command.
     */
    completers = {
        'cd': alias('ls', 'completers'),
        'ls': function(path) {
            if (typeof(path) !== 'string') return [];
            // absolute path?
            var absPath = false;
            if (path.charAt(0) === '/') {
                path = path.substr(1);
                absPath = true;
            }
            // split in directory and basename
            var dirName = path.split('/'),
                baseName = dirName.pop();
            if (!baseName) baseName = '';
            if (baseName === '.' || baseName === '..') {
                dirName.push(baseName);
                baseName = '';
            }
            dirName = dirName.join('/');
            if (absPath && dirName === '') {
                dirName = '/';
            }
            // use this to call ls and filter the results
            var lsResult = cmds.ls(dirName);
            if (typeof(lsResult) === 'string') {
                return lsResult; // error message
            }
            return helpers.filterPrefixed(baseName, lsResult).
                map(function(c) {
                    if (dirName !== '') {
                        return dirName + (dirName === '/' ? '' : '/') + c;
                    }
                    return c;
                });
        },
        'theme': function(name) {
            var themeNames = [];
            $('link[rel$=stylesheet]').each(function() {
                themeNames.push($(this).attr('title').match(/-(\w+)$/)[1]);
            });
            return helpers.filterPrefixed(name, themeNames);
        }
    },

    /**
     * Helper function definitions.
     */
    helpers = {
        dirDiff: function(dirA, dirB) {
            var dA = helpers.trimSlashes(dirA).split('/'),
                dB = helpers.trimSlashes(dirB).split('/'),
                diff = [],
                offset = 0;
            while (dA.length > offset && dB.length > offset &&
                    dA[offset] === dB[offset]) {
                offset++;
            }
            for (var i = offset; i < dA.length; i++) { diff.push('..'); }
            for (var i = offset; i < dB.length; i++) { diff.push(dB[i]); }
            if (diff.length === 0) { diff.push('.'); }
            return diff.join('/');
        },
        dirResolve: function(dir, subDir) {
            if (dir.charAt(0) !== '/') {
                console.error('dirResolve can only resolve paths from absolute paths.');
            }
            var d = dir.split('/'),
                lastSlash = (d[d.length - 1] === '');
            if (lastSlash) {
                d.pop();
            }
            // remove first empty component, since we asserted that dir is absolute
            d.splice(0, 1);

            subDir = (subDir === '' ? [] : subDir.split('/'));
            for (var i = 0; i < subDir.length; i++) {
                if (subDir[i] === '') {
                    if (i === 0) {
                        d = ['']; // absolute path support
                    }
                    continue;
                }
                if (subDir[i] === '.') continue;
                if (subDir[i] === '..') {
                    if (typeof(d.pop()) === 'undefined') {
                        return null;
                    }
                } else {
                    d.push(subDir[i]);
                }
            }

            if (lastSlash) {
                d.push('');
            }
            var result = d.join('/');
            if (result.charAt(0) !== '/') {
                result = '/' + result;
            }
            return result;
        },
        filterPrefixed: function(key, haystack) {
            var matches = [];
            for (var i = 0; i < haystack.length; i++) {
                if (haystack[i].indexOf(key) === 0) {
                    matches.push(haystack[i]);
                }
            }
            return matches;
        },
        trimSlashes: function(str) {
            return str.replace(/\/^|\/$/g, '');
        }
    };

    /**
     * Initialize console on document load.
     */
    $(function() {
        // calculate the width of a non-breaking space
        spaceWidth = getTextWidth(' ');

        if ($c.size() > 0) {
            var initialText = $fi.text(),
                feedbackUp = $f.text();
            // enable toggling the console
            $(window).on('keydown', onKeyDown);
            // enable console writing
            $i.on('keyup', onKeyUp);
            $i.on('input', function(e) {
                // in case a user left with alt+tab
                cEnabled = true;
                $c.addClass('active');
                // update input contents
                cInput = $i.val();
                updateInput();
                updateCursorPosition();
            });
            // update state if console is blurred/focused
            $fi.on('focus', function() {
                // in case focus happens outside our control (e.g. by clicking)
                cEnabled = true;
                $c.addClass('active');
                updateInput();
                setFeedback(feedbackUp);
                $i.focus();
                updateCursorPosition();
            });
            $i.on('blur', function() {
                cEnabled = false; // in case blur happens outside our control
                $c.removeClass('active');
                $fi.text(initialText);
                feedbackUp = $f.text();
                setFeedback('');
                updateCursorPosition();
            });
        }
    });

}(jQuery, window, document));
<commit_msg>Make Desh case insensitive<commit_after>---
yuiCompress: !!bool false
---
(function($, window, document) {

    var $c = $('#footer'),
        $f = $c.find('#feedback'),
        $i = $c.find('#input'),
        $fi = $c.find('#fake-input'),
        $cursor = $c.find('#cursor'),
        $dummy = $('<div />', {id: 'dummy'}).appendTo('body'),
        cEnabled = false,
        cInput = '',
        cLastKeyWasTab = false,
        cMaxLen = 40,
        cToggleKey = 192, // this is ~
        cmds = {}, // defined later
        spaceWidth = 0, // defined later
        siteMap = <%= get_sitemap %>, // used for completion (ls/cd)
        siteMapIds = <%= get_sitemap_ids %>, // used for navigation

    /**
     * Useful to create alias functions.
     */
    alias = function(cmd, type) {
        return function() {
            if (type === 'completers') {
                return completers[cmd].apply(this, arguments);
            }
            return cmds[cmd].apply(this, arguments);
        }
    },

    /**
     * Given a command, return options what the command could be.
     */
    completeCmd = function(cmd) {
        var cmdSplit = cmd.split(/\s+/g);
        if (cmdSplit.length === 1) {
            return helpers.filterPrefixed(cmd, Object.keys(cmds));
        } else if (completers.hasOwnProperty(cmdSplit[0])) {
            cmd = cmdSplit.splice(0, 1);
            var matches = completers[cmd].apply(this, cmdSplit);
            matches.prefix = cmd;
            return matches;
        }
        return [];
    },

    /**
     * Encode HTML entities in the given string, and make every space a
     * non-breaking one. Useful when showing user input to user.
     *
     * \param str The string to encode.
     * \param plainStr Optional boolean, default \c false. When true, return
     *            a string with plain characters instead of HTML escape
     *            sequences. The string will be Unicode.
     */
    encode = function(str, plainStr) {
        if (typeof(str) !== 'string') {
            return '';
        }
        if (typeof(plainStr) !== 'boolean') {
            plainStr = false;
        }
        return str.replace(/[\u00A0-\u9999<>\& ]/gim, function(i) {
            if (i === ' ') {
                return (plainStr ? '\u00A0' : ' ');
            }
            return (plainStr ? i.charAt(0) : '&#' + i.charCodeAt(0) + ';');
        });
    },

    /**
     * Compute the width of the given string using a dummy element.
     */
    getTextWidth = function(str) {
        // use a cached dummy
        return $dummy.html(str).width();
    },

    /**
     * Triggered for \c keypress events on the window. Should toggle the
     * console status for the activation key.
     */
    onKeyDown = function(e) {
        // handle tab here, before it changes the focus
        // also respond to global toggle key events
        if (e.which === 9 || e.which === cToggleKey) {
            onKeyUp(e);
        }
    },

    /**
     * Handler for key press events in the console input. Should only do
     * something if the console is enabled.
     */
    onKeyUp = function(e) {
        if (cEnabled && e.which === cToggleKey) {
            return;
        }
        // see if we should handle this key press
        var handled = true,
            thisKeyIsTab = false;
        switch (e.which) {
            case cToggleKey: // is this to blur the console?
                cEnabled = !cEnabled;
                if (cEnabled) {
                    $fi.focus();
                } else {
                    $i.blur();
                }
                break;
            case 8: // backspace
                // we only update the input after the switch, that is all
                break;
            case 9: // tab
                thisKeyIsTab = true;
                if (e.type === 'keyup') {
                    // already handled in keydown
                    break;
                }
                var matches = completeCmd(cInput);
                if (matches.length === 1) {
                    setInput((matches.prefix ? matches.prefix + ' ' : '') +
                        matches[0] + ' ');
                } else if (matches.length > 1 && cLastKeyWasTab) {
                    setFeedback(matches);
                }
                break;
            case 13: // enter
                setFeedback(parseCmd(cInput));
                cmds.clear();
                break;
            default:
                handled = false;
        }
        // did we do anything? then mark event as handled
        if (handled) {
            e.preventDefault();
            e.stopPropagation();
            cLastKeyWasTab = thisKeyIsTab;
            // update text in input
            updateInput();
        }
        // always update cursor position, because we do not explicitly handle
        // home, end, arrow keys, et cetera
        updateCursorPosition();
    },

    /**
     * See if we know the given command and act on it.
     */
    parseCmd = function(cmd) {
        cmd = cmd.trim().split(/\s+/g);
        if (cmd.length > 0) {
            if (cmds.hasOwnProperty(cmd[0])) {
                var cmdNm = cmd.splice(0, 1);
                return cmds[cmdNm[0]].apply(this, cmd);
            }
        }
        return 'desh: Unknown command "' + cmd + '".';
    },

    /**
     * Shorthand function to create a function that returns its argument.
     */
    s = function(str) {
        return function() {
            return str;
        }
    },

    /**
     * Set the contents of the feedback.
     */
    setFeedback = function(str) {
        if (typeof(str) !== 'string') {
            if ((Array.isArray && Array.isArray(str)) ||
                    str instanceof Array) {
                str = str.join('    ');
            } else {
                str = '';
            }
        }
        $f.html(encode(str.toLowerCase()));
        $c.toggleClass('tall', (str != ''));
    },

    /**
     * Set the contents of the input.
     *
     * Both the hidden and the fake one. Does need a call to updateInput to
     * see the effect though.
     */
    setInput = function(str) {
        cInput = str.toLowerCase();
        $i.val(cInput);
    },

    /**
     * Update the position of the cursor so it matches the position of the
     * cursor in the hidden input element.
     */
    updateCursorPosition = function() {
        // position cursor absolute iff console is enabled
        $cursor.toggleClass('dynamic', cEnabled);
        if (cEnabled) {
            // determine cursor position
            var input = $i.get(0),
                selS = input.selectionStart,
                selE = input.selectionEnd,
                lastChar = cInput.substr(selE, 1);
            // selection is not supported, only cursor movement
            if (selE !== selS) {
                input.selectionStart = selE;
            }
            // get the length of the string up to the cursor
            $cursor.css('margin-left', getTextWidth(encode('>' + cInput.substr(0, selE))));
            if (lastChar === '') {
                $cursor.html(' ').css('width', '.7em');
            } else if (lastChar === ' ') {
                $cursor.html(' ').css('width', spaceWidth);
            } else {
                $cursor.text(lastChar).css('width', '');
            }
        } else {
            $cursor.html(' ').css({
                marginLeft: '',
                width: ''
            });
        }
    },

    /**
     * Update input text from the global \c cInput variable.
     */
    updateInput = function() {
        $fi.html(encode(cInput));
    },

    /**
     * Command definitions. Hey. Cheater! Reading the source and all that.
     * Well. Have fun, hacker :-)
     */
    cmds = {
        'about': alias('help'),
        'about-me': s('This is desh, the Denvelop Shell, programmed by Thom.'),
        'cd': function(path) {
            if (typeof(path) === 'undefined') {
                path = '/';
            }
            // make path absolute
            path = helpers.dirResolve(location.pathname, path);
            // if the path can be found in the sitemap, go there, otherwise error
            if (siteMap.indexOf(path) === -1) {
                return 'desh: Invalid path.';
            }
            // navigate to page
            $(window).trigger('denvelop-navigate', [path, siteMapIds[path]]);
        },
        'clear': function() { setInput(''); updateInput(); },
        'help': s('desh: This is a tiny console for geeks. No room for ' +
                    'further help, sorry.'),
        'ls': function(path) {
            var cwd = location.pathname,
                items = [], i, l, item, itemIndex;
            if (typeof(path) === 'string') {
                cwd = helpers.dirResolve(cwd, path);
                if (cwd === null) {
                    return 'desh: Invalid path.';
                }
            }
            // consider all items the user may mean
            for (i = 0, l = siteMap.length; i < l; ++i) {
                if (siteMap[i].startsWith(cwd)) {
                    item = siteMap[i].substr(cwd.length);
                    // the item itself is referred to with a single dot
                    if (item === '') {
                        item = '.';
                    }
                    // we do not list directories recursively
                    itemIndex = item.indexOf('/');
                    if (itemIndex < 0 || itemIndex === item.length - 1) {
                        items.push(item);
                    }
                }
            }
            // if may be possible to move up a directory
            if (cwd !== '/' && items.indexOf('.') !== -1) {
                items.push('..');
            }
            // present items in sorted order
            items.sort();
            return items;
        },
        'theme': function(name) {
            $(window).one('denvelop-theme-switched', function(e, switched, newTheme) {
                if (!switched) {
                    if (typeof(name) === 'undefined') {
                        setFeedback('desh: Current theme is "' + newTheme + '".');
                    } else {
                        setFeedback('desh: Invalid theme name.');
                    }
                }
            });
            $(window).trigger('denvelop-theme-switch', [name]);
        }
    },

    /**
     * Tab completion functions per command.
     */
    completers = {
        'cd': alias('ls', 'completers'),
        'ls': function(path) {
            if (typeof(path) !== 'string') return [];
            // absolute path?
            var absPath = false;
            if (path.charAt(0) === '/') {
                path = path.substr(1);
                absPath = true;
            }
            // split in directory and basename
            var dirName = path.split('/'),
                baseName = dirName.pop();
            if (!baseName) baseName = '';
            if (baseName === '.' || baseName === '..') {
                dirName.push(baseName);
                baseName = '';
            }
            dirName = dirName.join('/');
            if (absPath && dirName === '') {
                dirName = '/';
            }
            // use this to call ls and filter the results
            var lsResult = cmds.ls(dirName);
            if (typeof(lsResult) === 'string') {
                return lsResult; // error message
            }
            return helpers.filterPrefixed(baseName, lsResult).
                map(function(c) {
                    if (dirName !== '') {
                        return dirName + (dirName === '/' ? '' : '/') + c;
                    }
                    return c;
                });
        },
        'theme': function(name) {
            var themeNames = [];
            $('link[rel$=stylesheet]').each(function() {
                themeNames.push($(this).attr('title').match(/-(\w+)$/)[1]);
            });
            return helpers.filterPrefixed(name, themeNames);
        }
    },

    /**
     * Helper function definitions.
     */
    helpers = {
        dirDiff: function(dirA, dirB) {
            var dA = helpers.trimSlashes(dirA).split('/'),
                dB = helpers.trimSlashes(dirB).split('/'),
                diff = [],
                offset = 0;
            while (dA.length > offset && dB.length > offset &&
                    dA[offset] === dB[offset]) {
                offset++;
            }
            for (var i = offset; i < dA.length; i++) { diff.push('..'); }
            for (var i = offset; i < dB.length; i++) { diff.push(dB[i]); }
            if (diff.length === 0) { diff.push('.'); }
            return diff.join('/');
        },
        dirResolve: function(dir, subDir) {
            if (dir.charAt(0) !== '/') {
                console.error('dirResolve can only resolve paths from absolute paths.');
            }
            var d = dir.split('/'),
                lastSlash = (d[d.length - 1] === '');
            if (lastSlash) {
                d.pop();
            }
            // remove first empty component, since we asserted that dir is absolute
            d.splice(0, 1);

            subDir = (subDir === '' ? [] : subDir.split('/'));
            for (var i = 0; i < subDir.length; i++) {
                if (subDir[i] === '') {
                    if (i === 0) {
                        d = ['']; // absolute path support
                    }
                    continue;
                }
                if (subDir[i] === '.') continue;
                if (subDir[i] === '..') {
                    if (typeof(d.pop()) === 'undefined') {
                        return null;
                    }
                } else {
                    d.push(subDir[i]);
                }
            }

            if (lastSlash) {
                d.push('');
            }
            var result = d.join('/');
            if (result.charAt(0) !== '/') {
                result = '/' + result;
            }
            return result;
        },
        filterPrefixed: function(key, haystack) {
            var matches = [];
            for (var i = 0; i < haystack.length; i++) {
                if (haystack[i].indexOf(key) === 0) {
                    matches.push(haystack[i]);
                }
            }
            return matches;
        },
        trimSlashes: function(str) {
            return str.replace(/\/^|\/$/g, '');
        }
    };

    /**
     * Initialize console on document load.
     */
    $(function() {
        // calculate the width of a non-breaking space
        spaceWidth = getTextWidth(' ');

        if ($c.size() > 0) {
            var initialText = $fi.text(),
                feedbackUp = $f.text();
            // enable toggling the console
            $(window).on('keydown', onKeyDown);
            // enable console writing
            $i.on('keyup', onKeyUp);
            $i.on('input', function(e) {
                // in case a user left with alt+tab
                cEnabled = true;
                $c.addClass('active');
                // update input contents
                cInput = $i.val().toLowerCase();
                updateInput();
                updateCursorPosition();
            });
            // update state if console is blurred/focused
            $fi.on('focus', function() {
                // in case focus happens outside our control (e.g. by clicking)
                cEnabled = true;
                $c.addClass('active');
                updateInput();
                setFeedback(feedbackUp);
                $i.focus();
                updateCursorPosition();
            });
            $i.on('blur', function() {
                cEnabled = false; // in case blur happens outside our control
                $c.removeClass('active');
                $fi.text(initialText);
                feedbackUp = $f.text();
                setFeedback('');
                updateCursorPosition();
            });
        }
    });

}(jQuery, window, document));
<|endoftext|>


<commit_before>/*
Copyright 2016 Brazil Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(function (wcjs, renderer) {
  'use strict';

  angular.module('app')
    .factory('PlayerService', PlayerService);

  function PlayerService(
    $rootScope,
    $window,
    $q,
    CommonService,
    playerRateLimit
  ) {
    var service = {
      player: null,
      renderContext: null,
      init: init,
      suspend: suspend,
      playing: playing,
      length: length,
      state: state,
      position: position,
      time: time,
      volume: volume,
      mute: mute,
      rate: rate,
      play: play,
      pause: pause,
      togglePause: togglePause,
      stop: stop,
      toggleMute: toggleMute,
      close: close,
      audioTrackCount: audioTrackCount,
      audioTrack: audioTrack,
      toggleAudioTrack: toggleAudioTrack,
      audioChannel: audioChannel,
      audioDelay: audioDelay,
      videoTrackCount: videoTrackCount,
      videoTrack: videoTrack,
      toggleVideoTrack: toggleVideoTrack,
      deinterlace: deinterlace,
      subtitlesCount: subtitlesCount,
      subtitlesTrack: subtitlesTrack,
      toggleSubtitlesTrack: toggleSubtitlesTrack,
      subtitlesDelay: subtitlesDelay,
      aspectRatio: aspectRatio,
      normalSpeed: normalSpeed,
      speedUp: speedUp,
      speedDown: speedDown,
      jumpForward: jumpForward,
      jumpBackward: jumpBackward,
      increaseVolume: increaseVolume,
      decreaseVolume: decreaseVolume,
      setScreenText: setScreenText,
      getScreenText: getScreenText,
      formattedTime: formattedTime,
      formattedLength: formattedLength,
      overwriteLength: overwriteLength,
      preseekTime: preseekTime
    };
    var props = {
      overwriteLength: 0,
      screenText: '',
      aspectRatio: '16:9',
      deinterlace: 'discard',
      settingTextEnabled: false,
      deinterlaceList: [
        'blend',
        'bob',
        'discard',
        'linear',
        'mean',
        'x',
        'yadif',
        'yadif2x'
      ]
    };

    $rootScope.$watchGroup([
      function () {
        return mute();
      },
      function () {
        return volume();
      },
      function () {
        return deinterlace();
      },
      function () {
        return aspectRatio();
      }
    ], function () {
      saveSetting();
    });
    $rootScope.$on('$destroy',
      $rootScope.$on('Player.FrameReady', function (event, frame) {
        service.renderContext.render(
          frame,
          frame.width,
          frame.height,
          frame.uOffset,
          frame.vOffset
        );
      })
    );
    $rootScope.$on('$destroy',
      $rootScope.$on('Player.MediaChanged', function () {
        var deregister1 = null;
        props.settingTextEnabled = false;
        deregister1 = $rootScope.$on('Player.Playing',
          function () {
            var deregister2 = $rootScope.$on('Player.PositionChanged',
              function () {
                loadSetting();
                time(preseekTime());
                preseekTime(0);
                props.settingTextEnabled = true;
                deregister2();
              });
            deregister1();
          });
      })
    );

    angular.element($window).on('unload', function () {
      close();
    });

    return service;

    function createEmitter(name) {
      return function (arg) {
        $rootScope.$broadcast(name, arg);
        $rootScope.$digest();
      };
    }

    function saveSetting() {
      var setting = {};
      setting.mute = mute();
      setting.volume = volume();
      setting.deinterlace = deinterlace();
      setting.aspectRatio = aspectRatio();
      CommonService.saveLocalStorage('player', setting);
      setting = {};
    }

    function loadSetting() {
      var setting = CommonService.loadLocalStorage('player') || {};
      mute(setting.mute);
      volume(setting.volume);
      if (angular.isDefined(setting.deinterlace)) {
        deinterlace(setting.deinterlace);
      } else {
        deinterlace(props.deinterlace);
      }
      if (angular.isDefined(setting.aspectRatio)) {
        aspectRatio(setting.aspectRatio);
      } else {
        aspectRatio(props.aspectRatio);
      }
    }

    function showSettingText(text) {
      if (props.settingTextEnabled) {
        setScreenText(text);
      }
    }

    function init(screen, forceCreate) {
      if (forceCreate || !service.player) {
        service.player = wcjs.createPlayer(['--network-caching', '5000']);
        service.player.onFrameReady = createEmitter('Player.FrameReady');
        service.player.onMediaChanged = createEmitter('Player.MediaChanged');
        service.player.onNothingSpecial = createEmitter('Player.NothingSpecial');
        service.player.onOpening = createEmitter('Player.Opening');
        service.player.onBuffering = createEmitter('Player.Buffering');
        service.player.onPlaying = createEmitter('Player.Playing');
        service.player.onPaused = createEmitter('Player.Paused');
        service.player.onForward = createEmitter('Player.Forward');
        service.player.onBackward = createEmitter('Player.Backward');
        service.player.onEncounteredError = createEmitter('Player.EncounteredError');
        service.player.onEndReached = createEmitter('Player.EndReached');
        service.player.onStopped = createEmitter('Player.Stopped');
        service.player.onTimeChanged = createEmitter('Player.TimeChanged');
        service.player.onPositionChanged = createEmitter('Player.PositionChanged');
        service.player.onSeekableChanged = createEmitter('Player.SeekableChanged');
        service.player.onPausableChanged = createEmitter('Player.PausableChanged');
        service.player.onLengthChanged = createEmitter('Player.LengthChanged');
      }
      service.renderContext = renderer.setupCanvas(screen);
      service.renderContext.fillBlack();
      $rootScope.$broadcast('Player.Ready');
    }

    function suspend() {
      service.player.stop();
      service.player.playlist.clear();
      service.renderContext.fillBlack();
    }

    function playing() {
      return service.player.playing;
    }

    function state() {
      return service.player.state;
    }

    function overwriteLength(value) {
      if (!isNaN(value)) {
        props.overwriteLength = parseInt(value, 10);
      }
      return props.overwriteLength;
    }

    function preseekTime(value) {
      if (!isNaN(value)) {
        props.preseekTime = parseInt(value, 10);
      }
      return props.preseekTime;
    }

    function length() {
      if (service.player.length === 0 && overwriteLength() > 0) {
        return overwriteLength();
      }
      return service.player.length;
    }

    function position(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.position = parseFloat(value);
        message = formattedTime();
        showSettingText(message);
      }
      return service.player.position;
    }

    function time(value) {
      var newTime = 0;

      if (!isNaN(value)) {
        newTime = parseInt(value, 10);
        if (service.player.time === 0 && overwriteLength() > 0) {
          position(newTime / overwriteLength());
        } else {
          service.player.time = newTime;
        }
      }
      if (service.player.time === 0 && overwriteLength() > 0) {
        return position() * overwriteLength();
      }
      return service.player.time;
    }

    function volume(value) {
      var message = '';

      if (!isNaN(value)) {
        if (value > 200) {
          service.player.volume = 200;
        } else if (value < 0) {
          service.player.volume = 0;
        } else {
          service.player.volume = parseInt(value, 10);
        }
        message = '音量 ' + volume() + '%';
        showSettingText(message);
      }
      return service.player.volume;
    }

    function mute(value) {
      if (angular.isDefined(value)) {
        service.player.mute = Boolean(value);
      }
      return service.player.mute;
    }

    function rate(value) {
      var newRate = 1;
      var message = '';

      if (!isNaN(value)) {
        newRate = parseFloat(value);
        if (newRate >= playerRateLimit) {
          service.player.input.rate = playerRateLimit;
        } else if (newRate > 64) {
          service.player.input.rate = 64;
        } else if (newRate < 1 / 32) {
          service.player.input.rate = 1 / 32;
        } else {
          service.player.input.rate = newRate;
        }
        message = '再生速度 x' + (Math.round(rate() * 100) / 100);
        showSettingText(message);
      }
      return service.player.input.rate;
    }

    function play(mrl) {
      service.player.play(mrl);
    }

    function pause() {
      service.player.pause();
    }

    function togglePause() {
      service.player.togglePause();
    }

    function stop() {
      service.player.stop();
    }

    function toggleMute() {
      service.player.toggleMute();
    }

    function close() {
      service.player.close();
    }

    function audioTrackCount() {
      return service.player.audio.count;
    }

    function audioTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.audio.track = parseInt(value, 10);
        if (audioTrack() > 0) {
          message = '音声' + audioTrack();
        } else {
          message = '音声無効';
        }
        showSettingText(message);
      }
      return service.player.audio.track;
    }

    function toggleAudioTrack() {
      var track = audioTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      audioTrack(track);
    }

    function audioChannel(value) {
      if (!isNaN(value)) {
        service.player.audio.channel = parseInt(value, 10);
      }
      return service.player.audio.channel;
    }

    function audioDelay(value) {
      if (!isNaN(value)) {
        service.player.audio.delay = parseInt(value, 10);
      }
      return service.player.audio.delay;
    }

    function videoTrackCount() {
      return service.player.video.count;
    }

    function videoTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.video.track = parseInt(value, 10);

        if (videoTrack() > 0) {
          message = '映像' + videoTrack();
        } else {
          message = '映像無効';
        }
        showSettingText(message);
      }
      return service.player.video.track;
    }

    function toggleVideoTrack() {
      var track = videoTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      videoTrack(track);
    }

    function deinterlace(value) {
      var isEnabled = false;

      if (angular.isDefined(value)) {
        isEnabled = props.deinterlaceList.filter(function (a) {
          return a === value;
        }).length > 0;
        if (isEnabled) {
          service.player.video.deinterlace.enable(value);
          props.deinterlace = value;
        } else {
          service.player.video.deinterlace.disable();
          props.deinterlace = '';
        }
      }
      return props.deinterlace;
    }

    function subtitlesCount() {
      return service.player.subtitles.count;
    }

    function subtitlesTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.subtitles.track = parseInt(value, 10);
        if (subtitlesTrack() > 0) {
          message = '字幕' + subtitlesTrack();
        } else {
          message = '字幕無効';
        }
        showSettingText(message);
      }
      return service.player.subtitles.track;
    }

    function toggleSubtitlesTrack() {
      var track = subtitlesTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      subtitlesTrack(track);
    }

    function subtitlesDelay(value) {
      if (!isNaN(value)) {
        service.player.subtitles.delay = parseInt(value, 10);
      }
      return service.player.subtitles.delay;
    }

    function aspectRatio(value) {
      if (angular.isDefined(value)) {
        props.aspectRatio = value;
      }
      return props.aspectRatio;
    }

    function normalSpeed() {
      rate(1);
    }

    function speedUp() {
      var newRate = rate();
      switch (Math.floor(newRate.toFixed(1))) {
        case 3:
        case 2:
          newRate += 1;
          break;
        case 1:
          newRate += 1 / 10;
          break;
        default:
          newRate *= 2;
      }
      rate(newRate);
    }

    function speedDown() {
      var newRate = rate();
      switch (Math.ceil(newRate.toFixed(1))) {
        case 4:
        case 3:
          newRate -= 1;
          break;
        case 2:
          newRate -= 1 / 10;
          break;
        default:
          newRate /= 2;
      }
      rate(newRate);
    }

    function jumpForward(value) {
      if (/^-?[0-9.]+ms/.test(value)) {
        time(time() + parseFloat(value));
      } else if (/^-?[0-9.]+s/.test(value)) {
        time(time() + (parseFloat(value) * 1000));
      } else {
        position(position() + parseFloat(value));
      }
    }

    function jumpBackward(value) {
      if (/^-?[0-9.]+ms/.test(value)) {
        time(time() - parseFloat(value));
      } else if (/^-?[0-9.]+s/.test(value)) {
        time(time() - (parseFloat(value) * 1000));
      } else {
        position(position() - parseFloat(value));
      }
    }

    function increaseVolume(value) {
      volume(volume() + value);
    }

    function decreaseVolume(value) {
      volume(volume() - value);
    }

    function setScreenText(text) {
      props.screenText = text;
    }

    function getScreenText() {
      var text = props.screenText;
      props.screenText = '';
      return text;
    }

    function formattedTime() {
      return CommonService.formatTime(time());
    }

    function formattedLength() {
      return CommonService.formatTime(length());
    }
  }
}(
  require(process.platform === 'linux' ? 'webchimera.js' : 'wcjs-prebuilt'),
  require('webgl-video-renderer')
));
<commit_msg>miyoutv: PlayerService修正<commit_after>/*
Copyright 2016 Brazil Ltd.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(function (wcjs, renderer) {
  'use strict';

  angular.module('app')
    .factory('PlayerService', PlayerService);

  function PlayerService(
    $rootScope,
    $window,
    $q,
    CommonService,
    playerRateLimit
  ) {
    var service = {
      player: null,
      renderContext: null,
      init: init,
      suspend: suspend,
      playing: playing,
      length: length,
      state: state,
      position: position,
      time: time,
      volume: volume,
      mute: mute,
      rate: rate,
      play: play,
      pause: pause,
      togglePause: togglePause,
      stop: stop,
      toggleMute: toggleMute,
      close: close,
      audioTrackCount: audioTrackCount,
      audioTrack: audioTrack,
      toggleAudioTrack: toggleAudioTrack,
      audioChannel: audioChannel,
      audioDelay: audioDelay,
      videoTrackCount: videoTrackCount,
      videoTrack: videoTrack,
      toggleVideoTrack: toggleVideoTrack,
      deinterlace: deinterlace,
      subtitlesCount: subtitlesCount,
      subtitlesTrack: subtitlesTrack,
      toggleSubtitlesTrack: toggleSubtitlesTrack,
      subtitlesDelay: subtitlesDelay,
      aspectRatio: aspectRatio,
      normalSpeed: normalSpeed,
      speedUp: speedUp,
      speedDown: speedDown,
      jumpForward: jumpForward,
      jumpBackward: jumpBackward,
      increaseVolume: increaseVolume,
      decreaseVolume: decreaseVolume,
      setScreenText: setScreenText,
      getScreenText: getScreenText,
      formattedTime: formattedTime,
      formattedLength: formattedLength,
      overwriteLength: overwriteLength,
      preseekTime: preseekTime
    };
    var props = {
      overwriteLength: 0,
      preseekTime: 0,
      screenText: '',
      aspectRatio: '16:9',
      deinterlace: 'discard',
      settingTextEnabled: false,
      deinterlaceList: [
        'blend',
        'bob',
        'discard',
        'linear',
        'mean',
        'x',
        'yadif',
        'yadif2x'
      ]
    };

    $rootScope.$watchGroup([
      function () {
        return mute();
      },
      function () {
        return volume();
      },
      function () {
        return deinterlace();
      },
      function () {
        return aspectRatio();
      }
    ], function () {
      saveSetting();
    });
    $rootScope.$on('$destroy',
      $rootScope.$on('Player.FrameReady', function (event, frame) {
        service.renderContext.render(
          frame,
          frame.width,
          frame.height,
          frame.uOffset,
          frame.vOffset
        );
      })
    );
    $rootScope.$on('$destroy',
      $rootScope.$on('Player.MediaChanged', function () {
        var deregister1 = null;
        props.settingTextEnabled = false;
        deregister1 = $rootScope.$on('Player.Playing',
          function () {
            var deregister2 = $rootScope.$on('Player.PositionChanged',
              function () {
                loadSetting();
                time(preseekTime());
                preseekTime(0);
                props.settingTextEnabled = true;
                deregister2();
              });
            deregister1();
          });
      })
    );

    angular.element($window).on('unload', function () {
      close();
    });

    return service;

    function createEmitter(name) {
      return function (arg) {
        $rootScope.$broadcast(name, arg);
        $rootScope.$digest();
      };
    }

    function saveSetting() {
      var setting = {};
      setting.mute = mute();
      setting.volume = volume();
      setting.deinterlace = deinterlace();
      setting.aspectRatio = aspectRatio();
      CommonService.saveLocalStorage('player', setting);
      setting = {};
    }

    function loadSetting() {
      var setting = CommonService.loadLocalStorage('player') || {};
      mute(setting.mute);
      volume(setting.volume);
      if (angular.isDefined(setting.deinterlace)) {
        deinterlace(setting.deinterlace);
      } else {
        deinterlace(props.deinterlace);
      }
      if (angular.isDefined(setting.aspectRatio)) {
        aspectRatio(setting.aspectRatio);
      } else {
        aspectRatio(props.aspectRatio);
      }
    }

    function showSettingText(text) {
      if (props.settingTextEnabled) {
        setScreenText(text);
      }
    }

    function init(screen, forceCreate) {
      if (forceCreate || !service.player) {
        service.player = wcjs.createPlayer(['--network-caching', '5000']);
        service.player.onFrameReady = createEmitter('Player.FrameReady');
        service.player.onMediaChanged = createEmitter('Player.MediaChanged');
        service.player.onNothingSpecial = createEmitter('Player.NothingSpecial');
        service.player.onOpening = createEmitter('Player.Opening');
        service.player.onBuffering = createEmitter('Player.Buffering');
        service.player.onPlaying = createEmitter('Player.Playing');
        service.player.onPaused = createEmitter('Player.Paused');
        service.player.onForward = createEmitter('Player.Forward');
        service.player.onBackward = createEmitter('Player.Backward');
        service.player.onEncounteredError = createEmitter('Player.EncounteredError');
        service.player.onEndReached = createEmitter('Player.EndReached');
        service.player.onStopped = createEmitter('Player.Stopped');
        service.player.onTimeChanged = createEmitter('Player.TimeChanged');
        service.player.onPositionChanged = createEmitter('Player.PositionChanged');
        service.player.onSeekableChanged = createEmitter('Player.SeekableChanged');
        service.player.onPausableChanged = createEmitter('Player.PausableChanged');
        service.player.onLengthChanged = createEmitter('Player.LengthChanged');
      }
      service.renderContext = renderer.setupCanvas(screen);
      service.renderContext.fillBlack();
      $rootScope.$broadcast('Player.Ready');
    }

    function suspend() {
      service.player.stop();
      service.player.playlist.clear();
      service.renderContext.fillBlack();
    }

    function playing() {
      return service.player.playing;
    }

    function state() {
      return service.player.state;
    }

    function overwriteLength(value) {
      if (!isNaN(value)) {
        props.overwriteLength = parseInt(value, 10);
      }
      return props.overwriteLength;
    }

    function preseekTime(value) {
      if (!isNaN(value)) {
        props.preseekTime = parseInt(value, 10);
      }
      return props.preseekTime;
    }

    function length() {
      if (service.player.length === 0 && overwriteLength() > 0) {
        return overwriteLength();
      }
      return service.player.length;
    }

    function position(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.position = parseFloat(value);
        message = formattedTime();
        showSettingText(message);
      }
      return service.player.position;
    }

    function time(value) {
      var newTime = 0;

      if (!isNaN(value)) {
        newTime = parseInt(value, 10);
        if (service.player.time === 0 && overwriteLength() > 0) {
          position(newTime / overwriteLength());
        } else {
          service.player.time = newTime;
        }
      }
      if (service.player.time === 0 && overwriteLength() > 0) {
        return position() * overwriteLength();
      }
      return service.player.time;
    }

    function volume(value) {
      var message = '';

      if (!isNaN(value)) {
        if (value > 200) {
          service.player.volume = 200;
        } else if (value < 0) {
          service.player.volume = 0;
        } else {
          service.player.volume = parseInt(value, 10);
        }
        message = '音量 ' + volume() + '%';
        showSettingText(message);
      }
      return service.player.volume;
    }

    function mute(value) {
      if (angular.isDefined(value)) {
        service.player.mute = Boolean(value);
      }
      return service.player.mute;
    }

    function rate(value) {
      var newRate = 1;
      var message = '';

      if (!isNaN(value)) {
        newRate = parseFloat(value);
        if (newRate >= playerRateLimit) {
          service.player.input.rate = playerRateLimit;
        } else if (newRate > 64) {
          service.player.input.rate = 64;
        } else if (newRate < 1 / 32) {
          service.player.input.rate = 1 / 32;
        } else {
          service.player.input.rate = newRate;
        }
        message = '再生速度 x' + (Math.round(rate() * 100) / 100);
        showSettingText(message);
      }
      return service.player.input.rate;
    }

    function play(mrl) {
      service.player.play(mrl);
    }

    function pause() {
      service.player.pause();
    }

    function togglePause() {
      service.player.togglePause();
    }

    function stop() {
      service.player.stop();
    }

    function toggleMute() {
      service.player.toggleMute();
    }

    function close() {
      service.player.close();
    }

    function audioTrackCount() {
      return service.player.audio.count;
    }

    function audioTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.audio.track = parseInt(value, 10);
        if (audioTrack() > 0) {
          message = '音声' + audioTrack();
        } else {
          message = '音声無効';
        }
        showSettingText(message);
      }
      return service.player.audio.track;
    }

    function toggleAudioTrack() {
      var track = audioTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      audioTrack(track);
    }

    function audioChannel(value) {
      if (!isNaN(value)) {
        service.player.audio.channel = parseInt(value, 10);
      }
      return service.player.audio.channel;
    }

    function audioDelay(value) {
      if (!isNaN(value)) {
        service.player.audio.delay = parseInt(value, 10);
      }
      return service.player.audio.delay;
    }

    function videoTrackCount() {
      return service.player.video.count;
    }

    function videoTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.video.track = parseInt(value, 10);

        if (videoTrack() > 0) {
          message = '映像' + videoTrack();
        } else {
          message = '映像無効';
        }
        showSettingText(message);
      }
      return service.player.video.track;
    }

    function toggleVideoTrack() {
      var track = videoTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      videoTrack(track);
    }

    function deinterlace(value) {
      var isEnabled = false;

      if (angular.isDefined(value)) {
        isEnabled = props.deinterlaceList.filter(function (a) {
          return a === value;
        }).length > 0;
        if (isEnabled) {
          service.player.video.deinterlace.enable(value);
          props.deinterlace = value;
        } else {
          service.player.video.deinterlace.disable();
          props.deinterlace = '';
        }
      }
      return props.deinterlace;
    }

    function subtitlesCount() {
      return service.player.subtitles.count;
    }

    function subtitlesTrack(value) {
      var message = '';

      if (!isNaN(value)) {
        service.player.subtitles.track = parseInt(value, 10);
        if (subtitlesTrack() > 0) {
          message = '字幕' + subtitlesTrack();
        } else {
          message = '字幕無効';
        }
        showSettingText(message);
      }
      return service.player.subtitles.track;
    }

    function toggleSubtitlesTrack() {
      var track = subtitlesTrack() + 1;
      if (track <= 0) {
        track = 1;
      }
      subtitlesTrack(track);
    }

    function subtitlesDelay(value) {
      if (!isNaN(value)) {
        service.player.subtitles.delay = parseInt(value, 10);
      }
      return service.player.subtitles.delay;
    }

    function aspectRatio(value) {
      if (angular.isDefined(value)) {
        props.aspectRatio = value;
      }
      return props.aspectRatio;
    }

    function normalSpeed() {
      rate(1);
    }

    function speedUp() {
      var newRate = rate();
      switch (Math.floor(newRate.toFixed(1))) {
        case 3:
        case 2:
          newRate += 1;
          break;
        case 1:
          newRate += 1 / 10;
          break;
        default:
          newRate *= 2;
      }
      rate(newRate);
    }

    function speedDown() {
      var newRate = rate();
      switch (Math.ceil(newRate.toFixed(1))) {
        case 4:
        case 3:
          newRate -= 1;
          break;
        case 2:
          newRate -= 1 / 10;
          break;
        default:
          newRate /= 2;
      }
      rate(newRate);
    }

    function jumpForward(value) {
      if (/^-?[0-9.]+ms/.test(value)) {
        time(time() + parseFloat(value));
      } else if (/^-?[0-9.]+s/.test(value)) {
        time(time() + (parseFloat(value) * 1000));
      } else {
        position(position() + parseFloat(value));
      }
    }

    function jumpBackward(value) {
      if (/^-?[0-9.]+ms/.test(value)) {
        time(time() - parseFloat(value));
      } else if (/^-?[0-9.]+s/.test(value)) {
        time(time() - (parseFloat(value) * 1000));
      } else {
        position(position() - parseFloat(value));
      }
    }

    function increaseVolume(value) {
      volume(volume() + value);
    }

    function decreaseVolume(value) {
      volume(volume() - value);
    }

    function setScreenText(text) {
      props.screenText = text;
    }

    function getScreenText() {
      var text = props.screenText;
      props.screenText = '';
      return text;
    }

    function formattedTime() {
      return CommonService.formatTime(time());
    }

    function formattedLength() {
      return CommonService.formatTime(length());
    }
  }
}(
  require(process.platform === 'linux' ? 'webchimera.js' : 'wcjs-prebuilt'),
  require('webgl-video-renderer')
));
<|endoftext|>


<commit_before>// @flow
/**
 * All About Olaf
 * Streaming Media page
 */

import React from 'react'
import {StyleSheet, SectionList} from 'react-native'

import * as c from '../../components/colors'
import {ListSeparator, ListSectionHeader} from '../../components/list'
import LoadingView from '../../components/loading'
import {NoticeView} from '../../components/notice'
import {TabBarIcon} from '../../components/tabbar-icon'
import {StreamRow} from './row'
import toPairs from 'lodash/toPairs'
import groupBy from 'lodash/groupBy'
import moment from 'moment-timezone'
import qs from 'querystring'
import {toLaxTitleCase as titleCase} from 'titlecase'
import type {StreamType} from './types'
import delay from 'delay'

const CENTRAL_TZ = 'America/Winnipeg'
const url = 'https://www.stolaf.edu/multimedia/api/collection'

const styles = StyleSheet.create({
  listContainer: {
    backgroundColor: c.white,
  },
})

export class StreamListView extends React.PureComponent {
  static navigationOptions = {
    tabBarLabel: 'Streaming',
    tabBarIcon: TabBarIcon('recording'),
  }

  state: {
    error: ?Error,
    loaded: boolean,
    refreshing: boolean,
    streams: Array<{title: string, data: Array<StreamType>}>,
  } = {
    error: null,
    loaded: false,
    refreshing: false,
    streams: [],
  }

  componentWillMount() {
    this.getStreams()
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.getStreams()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    const elapsed = Date.now() - start
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }

    this.setState(() => ({refreshing: false}))
  }

  getStreams = async (date: moment = moment.tz(CENTRAL_TZ)) => {
    try {
      const dateFrom = date.format('YYYY-MM-DD')
      const dateTo = date.add(1, 'month').format('YYYY-MM-DD')

      let params = {
        class: 'upcoming',
        sort: 'ascending',
        // eslint-disable-next-line camelcase
        date_from: dateFrom,
        // eslint-disable-next-line camelcase
        date_to: dateTo,
      }

      const streamsAPI = `${url}?${qs.stringify(params)}`
      const data = await fetchJson(streamsAPI)
      const streams = data.results

      // force title-case on the stream types, to prevent not-actually-duplicate headings
      const processed = streams
        .filter(stream => stream.category !== 'athletics')
        .map(stream => {
          const date = moment(stream.starttime, 'YYYY-MM-DD HH:mm')
          return {
            ...stream,
            category: titleCase(stream.category),
            date: date,
            $groupBy: date.format('dddd, MMMM Do'),
          }
        })

      const grouped = groupBy(processed, j => j.$groupBy)
      const mapped = toPairs(grouped).map(([title, data]) => ({title, data}))

      this.setState(() => ({
        error: null,
        loaded: true,
        streams: mapped,
      }))
    } catch (error) {
      this.setState(() => ({error: error.message}))
      console.warn(error)
    }
  }

  keyExtractor = (item: StreamType) => item.eid

  renderSectionHeader = ({section: {title}}: any) => (
    <ListSectionHeader title={title} />
  )

  renderItem = ({item}: {item: StreamType}) => <StreamRow stream={item} />

  render() {
    if (!this.state.loaded) {
      return <LoadingView />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    return (
      <SectionList
        ListEmptyComponent={<NoticeView text="No Streams" />}
        renderSectionHeader={this.renderSectionHeader}
        sections={(this.state.streams: any)}
        ItemSeparatorComponent={ListSeparator}
        keyExtractor={this.keyExtractor}
        style={styles.listContainer}
        refreshing={this.state.refreshing}
        onRefresh={this.refresh}
        renderItem={this.renderItem}
      />
    )
  }
}
<commit_msg>clean up how Streams knows about loading state<commit_after>// @flow
/**
 * All About Olaf
 * Streaming Media page
 */

import React from 'react'
import {StyleSheet, SectionList} from 'react-native'

import * as c from '../../components/colors'
import {ListSeparator, ListSectionHeader} from '../../components/list'
import LoadingView from '../../components/loading'
import {NoticeView} from '../../components/notice'
import {TabBarIcon} from '../../components/tabbar-icon'
import {StreamRow} from './row'
import toPairs from 'lodash/toPairs'
import groupBy from 'lodash/groupBy'
import moment from 'moment-timezone'
import qs from 'querystring'
import {toLaxTitleCase as titleCase} from 'titlecase'
import type {StreamType} from './types'
import delay from 'delay'

const CENTRAL_TZ = 'America/Winnipeg'
const url = 'https://www.stolaf.edu/multimedia/api/collection'

const styles = StyleSheet.create({
  listContainer: {
    backgroundColor: c.white,
  },
})

export class StreamListView extends React.PureComponent {
  static navigationOptions = {
    tabBarLabel: 'Streaming',
    tabBarIcon: TabBarIcon('recording'),
  }

  state: {
    error: ?Error,
    loading: boolean,
    refreshing: boolean,
    streams: Array<{title: string, data: Array<StreamType>}>,
  } = {
    error: null,
    loading: true,
    refreshing: false,
    streams: [],
  }

  componentWillMount() {
    this.getStreams().then(() => {
      this.setState(() => ({loading: false}))
    })
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.getStreams()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    const elapsed = Date.now() - start
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }

    this.setState(() => ({refreshing: false}))
  }

  getStreams = async (date: moment = moment.tz(CENTRAL_TZ)) => {
    try {
      const dateFrom = date.format('YYYY-MM-DD')
      const dateTo = date.add(1, 'month').format('YYYY-MM-DD')

      let params = {
        class: 'upcoming',
        sort: 'ascending',
        // eslint-disable-next-line camelcase
        date_from: dateFrom,
        // eslint-disable-next-line camelcase
        date_to: dateTo,
      }

      const streamsAPI = `${url}?${qs.stringify(params)}`
      const data = await fetchJson(streamsAPI)
      const streams = data.results

      // force title-case on the stream types, to prevent not-actually-duplicate headings
      const processed = streams
        .filter(stream => stream.category !== 'athletics')
        .map(stream => {
          const date = moment(stream.starttime, 'YYYY-MM-DD HH:mm')
          return {
            ...stream,
            category: titleCase(stream.category),
            date: date,
            $groupBy: date.format('dddd, MMMM Do'),
          }
        })

      const grouped = groupBy(processed, j => j.$groupBy)
      const mapped = toPairs(grouped).map(([title, data]) => ({title, data}))

      this.setState(() => ({error: null, streams: mapped}))
    } catch (error) {
      this.setState(() => ({error: error.message}))
      console.warn(error)
    }
  }

  keyExtractor = (item: StreamType) => item.eid

  renderSectionHeader = ({section: {title}}: any) => (
    <ListSectionHeader title={title} />
  )

  renderItem = ({item}: {item: StreamType}) => <StreamRow stream={item} />

  render() {
    if (this.state.loading) {
      return <LoadingView />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    return (
      <SectionList
        ListEmptyComponent={<NoticeView text="No Streams" />}
        renderSectionHeader={this.renderSectionHeader}
        sections={(this.state.streams: any)}
        ItemSeparatorComponent={ListSeparator}
        keyExtractor={this.keyExtractor}
        style={styles.listContainer}
        refreshing={this.state.refreshing}
        onRefresh={this.refresh}
        renderItem={this.renderItem}
      />
    )
  }
}
<|endoftext|>


<commit_before>import React, {Component} from 'react'
import PropTypes from 'prop-types'

import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'

@ErrorHandling
class ColorDropdown extends Component {
  constructor(props) {
    super(props)

    this.state = {
      visible: false,
    }
  }

  handleToggleMenu = () => {
    const {disabled} = this.props

    if (disabled) {
      return
    }
    this.setState({visible: !this.state.visible})
  }

  handleClickOutside = () => {
    this.setState({visible: false})
  }

  handleColorClick = color => () => {
    this.props.onChoose(color)
    this.setState({visible: false})
  }

  render() {
    const {visible} = this.state
    const {colors, selected, disabled, stretchToFit} = this.props

    const dropdownClassNames = classnames('color-dropdown', {
      open: visible,
      'color-dropdown--stretch': stretchToFit,
    })
    const toggleClassNames = classnames(
      'btn btn-sm btn-default color-dropdown--toggle',
      {active: visible, 'color-dropdown__disabled': disabled}
    )

    return (

{selected.name}

        {visible ? (

            <FancyScrollbar autoHide={false} autoHeight={true}>
              {colors.map((color, i) => (

                  {color.name}

              ))}
            </FancyScrollbar>

        ) : null}

    )
  }
}

const {arrayOf, bool, func, shape, string} = PropTypes

ColorDropdown.propTypes = {
  selected: shape({
    hex: string.isRequired,
    name: string.isRequired,
  }).isRequired,
  onChoose: func.isRequired,
  colors: arrayOf(
    shape({
      hex: string.isRequired,
      name: string.isRequired,
    }).isRequired
  ).isRequired,
  stretchToFit: bool,
  disabled: bool,
}

export default OnClickOutside(ColorDropdown)
<commit_msg>Fix color dropdown by adding maxHeight attribute<commit_after>import React, {Component} from 'react'
import PropTypes from 'prop-types'

import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'src/shared/constants/index'

@ErrorHandling
class ColorDropdown extends Component {
  constructor(props) {
    super(props)

    this.state = {
      visible: false,
    }
  }

  handleToggleMenu = () => {
    const {disabled} = this.props

    if (disabled) {
      return
    }
    this.setState({visible: !this.state.visible})
  }

  handleClickOutside = () => {
    this.setState({visible: false})
  }

  handleColorClick = color => () => {
    this.props.onChoose(color)
    this.setState({visible: false})
  }

  render() {
    const {visible} = this.state
    const {colors, selected, disabled, stretchToFit} = this.props

    const dropdownClassNames = classnames('color-dropdown', {
      open: visible,
      'color-dropdown--stretch': stretchToFit,
    })
    const toggleClassNames = classnames(
      'btn btn-sm btn-default color-dropdown--toggle',
      {active: visible, 'color-dropdown__disabled': disabled}
    )

    return (

{selected.name}

        {visible ? (

            <FancyScrollbar
              autoHide={false}
              autoHeight={true}
              maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
            >
              {colors.map((color, i) => (

                  {color.name}

              ))}
            </FancyScrollbar>

        ) : null}

    )
  }
}

const {arrayOf, bool, func, shape, string} = PropTypes

ColorDropdown.propTypes = {
  selected: shape({
    hex: string.isRequired,
    name: string.isRequired,
  }).isRequired,
  onChoose: func.isRequired,
  colors: arrayOf(
    shape({
      hex: string.isRequired,
      name: string.isRequired,
    }).isRequired
  ).isRequired,
  stretchToFit: bool,
  disabled: bool,
}

export default OnClickOutside(ColorDropdown)
<|endoftext|>


<commit_before>/* Copyright 2017 Mozilla
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. */

'use strict';

const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const { Runtime } = Cu.import('resource://qbrt/modules/Runtime.jsm', {});
const { Services } = Cu.import('resource://gre/modules/Services.jsm', {});

function loadDevToolsWindow(target) {
  const devToolsWindow = Runtime.openDevTools(target);
  return new Promise(resolve => {
    devToolsWindow.addEventListener('DOMContentLoaded', resolve);
  })
  .then(() => {
    // Wait for the DevTools window's title to change, which indicates
    // that the window has successfully connected to its target.
    // We dump the window title so the test can check stdout to confirm
    // that the correct set of windows was loaded.
    return new Promise(resolve => {
      const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
          if (mutation.attributeName === 'title') {
            dump(`${devToolsWindow.document.title}\n`);
            observer.disconnect();
            resolve();
          }
        }
      });
      observer.observe(devToolsWindow.document.querySelector('window'), { attributes: true });
    });
  })
  .then(() => {
    return new Promise(resolve => {
      devToolsWindow.addEventListener('unload', resolve);
      devToolsWindow.close();
    })
  });
}

window.addEventListener('load', event => {
  Promise.resolve()
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-chrome'));
  })
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-content-primary'));
  })
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-content'));
  })
  .then(() => {
    return loadDevToolsWindow(window);
  })
  .then(() => {
    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
  })
  .catch(error => {
    dump(`${error}\n`);
    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
  });
});
<commit_msg>add missing trailing semicolon<commit_after>/* Copyright 2017 Mozilla
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. */

'use strict';

const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const { Runtime } = Cu.import('resource://qbrt/modules/Runtime.jsm', {});
const { Services } = Cu.import('resource://gre/modules/Services.jsm', {});

function loadDevToolsWindow(target) {
  const devToolsWindow = Runtime.openDevTools(target);
  return new Promise(resolve => {
    devToolsWindow.addEventListener('DOMContentLoaded', resolve);
  })
  .then(() => {
    // Wait for the DevTools window's title to change, which indicates
    // that the window has successfully connected to its target.
    // We dump the window title so the test can check stdout to confirm
    // that the correct set of windows was loaded.
    return new Promise(resolve => {
      const observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
          if (mutation.attributeName === 'title') {
            dump(`${devToolsWindow.document.title}\n`);
            observer.disconnect();
            resolve();
          }
        }
      });
      observer.observe(devToolsWindow.document.querySelector('window'), { attributes: true });
    });
  })
  .then(() => {
    return new Promise(resolve => {
      devToolsWindow.addEventListener('unload', resolve);
      devToolsWindow.close();
    });
  });
}

window.addEventListener('load', event => {
  Promise.resolve()
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-chrome'));
  })
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-content-primary'));
  })
  .then(() => {
    return loadDevToolsWindow(document.getElementById('browser-content'));
  })
  .then(() => {
    return loadDevToolsWindow(window);
  })
  .then(() => {
    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
  })
  .catch(error => {
    dump(`${error}\n`);
    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
  });
});
<|endoftext|>