NicovideoStoryboard

シークバーに出るサムネイルを並べて表示

当前为 2014-05-08 提交的版本,查看 最新版本

// ==UserScript==
// @name           NicovideoStoryboard
// @namespace      https://github.com/segabito/
// @description    シークバーに出るサムネイルを並べて表示
// @match          http://www.nicovideo.jp/watch/*
// @match          http://flapi.nicovideo.jp/api/getflv*
// @match          http://*.nicovideo.jp/smile*
// @grant          none
// @author         segabito macmoto
// @version        1.0.2
// ==/UserScript==

// ver 1.0.1  フルスクリーンモードで開いた時はプレイヤー領域を押し上げるようにした

(function() {
  var monkey = function() {
    var DEBUG = !true;
    var $ = window.jQuery, _ = window._;

    var __css__ = (function() {/*
      .xDomainLoaderFrame {
        position: fixed;
        top: -999px;
        left: -999px;
        width: 1px;
        height: 1px;
        border: 0;
      }

      .storyboardContainer {
        position: fixed;
        bottom: -300px;
        left: 0;
        right: 0;
        width: 100%;
        box-sizing: border-box;
        -moz-box-sizing: border-box;
        -webkit-box-sizing: border-box;
        background: #999;
        border: 2px outset #000;
        z-index: 9005;
        overflow: visible;
        border-radius: 10px 10px 0 0;
        border-width: 2px 2px 0;
        box-shadow: 0 -2px 2px #666;

        transition: bottom 0.5s ease-in-out;
      }

      .storyboardContainer.show {
        bottom: 0;
      }

      .storyboardContainer .storyboardInner {
        display: none;
        position: relative;
        text-align: center;
        overflow-x: scroll;
        white-space: nowrap;
        background: #222;
        margin: 4px 12px;
        border-style: inset;
        border-width: 2px 4px;
        border-radius: 10px 10px 0 0;
      }

      .storyboardContainer.success .storyboardInner {
        display: block;
      }

      .storyboardContainer .storyboardInner .boardList {
        overflow: hidden;
      }

      .storyboardContainer .boardList .board {
        display: inline-block;
        cursor: pointer;
        background-color: #101010;
      }

      .storyboardContainer.clicked .storyboardInner .boardList .board {
        cursor: wait;
        opacity: 0.5;
      }

      .storyboardContainer .boardList .board.lazyImage {
        background-color: #ccc;
      }
      .storyboardContainer .boardList .board.loadFail {
        background-color: #c99;
      }
      .storyboardContainer .boardList .board.lazyImage {
        cursor: wait;
      }

      .storyboardContainer .boardList .board > div {
        white-space: nowrap;
      }
      .storyboardContainer .boardList .board .border {
        box-sizing: border-box;
        -moz-box-sizing: border-box;
        -webkit-box-sizing: border-box;
        border-style: solid;
        border-color: #000 #333 #000 #999;
        border-width: 0     2px    0  2px;
        display: inline-block;
      }
      .storyboardContainer .boardList .board:hover .border {
        display: none; {* クリックできなくなっちゃうので苦し紛れの対策 もうちょっとマシな方法を考える *}
      }

      .storyboardContainer .storyboardHeader {
        position: relative;
        width: 100%;
      }
      .storyboardContainer .pointer {
        position: absolute;
        bottom: -15px;
        left: 50%;
        width: 32px;
        margin-left: -16px;
        color: #333;
        z-index: 9010;
        text-align: center;
      }

      .storyboardContainer .cursorTime {
        display: none;
        position: absolute;
        bottom: -30px;
        left: -999px;
        font-size: 10pt;
        border: 1px solid #000;
        z-index: 9010;
        background: #ffc;
      }
      .storyboardContainer:hover .cursorTime {
        display: block;
      }

      .storyboardContainer .setToDisable {
        position: absolute;
        display: inline-block;
        left: 250px;
        bottom: -32px;
        transition: bottom 0.3s ease-in-out;
      }
      .storyboardContainer:hover .setToDisable {
        bottom: 0;
      }

      .storyboardContainer .setToDisable button,
      .setToEnableButtonContainer button {
        background: none repeat scroll 0 0 #999;
        border-color: #666;
        border-radius: 18px 18px 0 0;
        border-style: solid;
        border-width: 2px 2px 0;
        width: 200px;
        overflow: auto;
        white-space: nowrap;
        cursor: pointer;
        box-shadow: 0 -2px 2px #666;
      }

      .full_with_browser .setToEnableButtonContainer button {
        box-shadow: none;
        color: #888;
        background: #000;
      }

      .full_with_browser .storyboardContainer .setToDisable,
      .full_with_browser .setToEnableButtonContainer {
        background: #000; {* Firefox対策 *}
      }

      .setToEnableButtonContainer button {
        width: 200px;
      }

      .storyboardContainer .setToDisable button:hover,
      .setToEnableButtonContainer:not(.loading):not(.fail) button:hover {
        background: #ccc;
        transition: none;
      }

      .storyboardContainer .setToDisable button.clicked,
      .setToEnableButtonContainer.loading button,
      .setToEnableButtonContainer.fail button,
      .setToEnableButtonContainer button.clicked {
        border-style: inset;
        box-shadow: none;
      }

      .setToEnableButtonContainer {
        position: fixed;
        z-index: 9003;
        left: 250px;
        bottom: 0px;
        transition: bottom 0.5s ease-in-out;
      }
      .setToEnableButtonContainer.loadingVideo {
        bottom: -50px;
      }

      .setToEnableButtonContainer.loading *,
      .setToEnableButtonContainer.loadingVideo *{
        cursor: wait;
        font-size: 80%;
      }
      .setToEnableButtonContainer.fail {
        color: #999;
        cursor: default;
        font-size: 80%;
      }

    */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');

    var storyboardTemplate = [
      '<div id="storyboardContainer" class="storyboardContainer">',
        '<div class="storyboardHeader">',
          '<div class="setToDisable"><button>閉じる ▼</button></div>',
          '<div class="pointer">▼</div>',
          '<div class="cursorTime"></div>',
        '</div>',

        '<div class="storyboardInner"></div>',
        '<div class="failMessage">',
        '</div>',
      '</div>',
      '',
    ''].join('');

 // マスコットキャラクターのサムネーヨちゃん
    var noThumbnailAA = (function() {/*
 ∧ ∧     ┌─────────────
 ( ´ー`)   < サムネーヨ
  \ <     └───/|────────
   \.\______//
     \       /
      ∪∪‾∪∪
    */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');

    var EventDispatcher = (function() {

      function AsyncEventDispatcher() {
        window.WatchApp.extend(
          this,
          AsyncEventDispatcher,
          window.WatchApp.ns.event.EventDispatcher);
      }

      window.WatchApp.mixin(AsyncEventDispatcher.prototype, {
//        addEventListener: function(name, func) {
//          console.log('%caddEventListener: ', 'background: red; color: white;', name, func);
//          if (!func) {
//            console.trace();
//          }
//          AsyncEventDispatcher.__super__.addEventListener.call(this, name, func);
//        },
        dispatchAsync: function() {
          var args = arguments;

          window.setTimeout($.proxy(function() {
            try {
              this.dispatchEvent.apply(this, args);
            } catch (e) {
              console.log(e);
            }
          }, this), 0);
        }
      });
      return AsyncEventDispatcher;
    })();

    window.NicovideoStoryboard =
      (function() {
        return {
          api: {},
          model: {},
          view: {},
          controller: {},
          external: {},
          event: {}
        };
      })();

//    var console =
//      (function(debug) {
//        var n = window._.noop;
//        return debug ? window.console : {log: n, trace: n, time: n, timeEnd: n};
//      })(DEBUG);
    var console = window.console;

    window.NicovideoStoryboard.event.windowEventDispatcher = (function() {
      var eventDispatcher = new EventDispatcher();

        var onMessage = function(event) {
          if (event.origin.indexOf('nicovideo.jp') < 0) return;
          try {
            var data = JSON.parse(event.data);
            if (data.id !== 'NicovideoStoryboard') { return; }

            eventDispatcher.dispatchEvent('onMessage', data.body, data.type);
          } catch (e) {
            console.log(
              '%cNicoVideoStoryboard.Error: window.onMessage  - ',
              'color: red; background: yellow',
              e
            );
            console.log('%corigin: ', 'background: yellow;', event.origin);
            console.log('%cdata: ',   'background: yellow;', event.data);
            console.trace();
          }
        };

        window.addEventListener('message', onMessage);

      return eventDispatcher;
    })();


    window.NicovideoStoryboard.external.watchController = (function() {
      var root = window.WatchApp.ns;
      var nicoPlayerConnector = root.init.PlayerInitializer.nicoPlayerConnector;
      var watchInfoModel      = root.model.WatchInfoModel.getInstance();
      var viewerInfoModel     = root.init.CommonModelInitializer.viewerInfoModel;
      var playerAreaConnector = root.init.PlayerInitializer.playerAreaConnector;
      var externalNicoplayer;

      var watchController = new EventDispatcher();

      var getVpos = function() {
        return nicoPlayerConnector.getVpos();
      };
      var setVpos = function(vpos) {
        nicoPlayerConnector.seekVideo(vpos);
      };

      var _isPlaying = null;
      var isPlaying = function() {
        if (_isPlaying !== null) {
          return _isPlaying;
        }
        if (!externalNicoplayer) {
          externalNicoplayer = $("#external_nicoplayer")[0];
        }
        var status = externalNicoplayer.ext_getStatus();
        return status === 'playing';
      };
      var play = function() {
        nicoPlayerConnector.playVideo();
      };
      var pause = function() {
        nicoPlayerConnector.stopVideo();
      };

      var isPremium = function() {
        return !!viewerInfoModel.isPremium;
      };

      var getWatchId = function() {// スレッドIDだったりsmXXXXだったり
        return watchInfoModel.v;
      };
      var getVideoId = function() {// smXXXXXX, soXXXXX など
        return watchInfoModel.id;
      };

      var popupMarquee = root.init.PopupMarqueeInitializer.popupMarqueeViewController;
      var popup = {
        message: function(text) {
          popupMarquee.onData(
            '<span style="background: black;">' + text + '</span>'
          );
        },
        alert: function(text) {
          popupMarquee.onData(
            '<span style="background: black; color: red;">' + text + '</span>'
          );
        }
      };

      playerAreaConnector.addEventListener('onVideoPlayed', function() {
        _isPlaying = true;
        watchController.dispatchEvent('onVideoPlayed');
      });
      playerAreaConnector.addEventListener('onVideoStopped', function() {
        _isPlaying = false;
        watchController.dispatchEvent('onVideoStopped');
      });

      playerAreaConnector.addEventListener('onVideoStarted', function() {
        _isPlaying = true;
        watchController.dispatchEvent('onVideoStarted');
      });
      playerAreaConnector.addEventListener('onVideoEnded', function() {
        _isPlaying = false;
        watchController.dispatchEvent('onVideoEnded');
      });

      playerAreaConnector.addEventListener('onVideoSeeked', function() {
        watchController.dispatchEvent('onVideoSeeked');
      });


      window.WatchApp.mixin(watchController, {
        getVpos: getVpos,
        setVpos: setVpos,

        isPlaying: isPlaying,
        play:  play,
        pause: pause,

        isPremium: isPremium,

        getWatchId: getWatchId,
        getVideoId: getVideoId,

        popup: popup
      });

      return watchController;
    })();


    window.NicovideoStoryboard.api.getflv = (function() {
      var BASE_URL = 'http://flapi.nicovideo.jp/api/getflv?v=';
      var loaderFrame, loaderWindow, cache = {};
      var eventDispatcher = window.NicovideoStoryboard.event.windowEventDispatcher;
      var getflv = new EventDispatcher();

      var parseInfo = function(q) {
        var info = {}, lines = q.split('&');
        $.each(lines, function(i, line) {
          var tmp = line.split('=');
          var key = window.unescape(tmp[0]), value = window.unescape(tmp[1]);
          info[key] = value;
        });
        return info;
      };

      var onMessage = function(data, type) {
        if (type !== 'getflv') { return; }
        var info = parseInfo(data.info), url = data.url;

        cache[url] = info;
        //console.log('getflv.onGetflvLoad', info);
        getflv.dispatchAsync('onGetflvLoad', info);
      };

      var initialize = function() {
        initialize = _.noop;

        console.log('%c initialize getflv', 'background: lightgreen;');

        loaderFrame = document.createElement('iframe');
        loaderFrame.name      = 'getflvLoader';
        loaderFrame.className = DEBUG ? 'xDomainLoaderFrame debug' : 'xDomainLoaderFrame';
        document.body.appendChild(loaderFrame);

        loaderWindow = loaderFrame.contentWindow;

        eventDispatcher.addEventListener('onMessage', onMessage);
      };

      var load = function(watchId) {
        initialize();
        var url = BASE_URL + watchId;
        //console.log('getflv: ', url);

        getflv.dispatchEvent('onGetflvLoadStart', watchId);
        if (cache[url]) {
          //console.log('%cgetflv cache exist', 'background: cyan', url);
          getflv.dispatchAsync('onGetflvLoad', cache[url]);
        } else {
          loaderWindow.location.replace(url);
        }
      };

      window.WatchApp.mixin(getflv, {
        load: load
      });

      return getflv;
    })();

    window.NicovideoStoryboard.api.thumbnailInfo = (function() {
      var getflv = window.NicovideoStoryboard.api.getflv;
      var loaderFrame, loaderWindow, cache = {};
      var eventDispatcher = window.NicovideoStoryboard.event.windowEventDispatcher;
      var thumbnailInfo = new EventDispatcher();

      var onGetflvLoad = function(info) {
        //console.log('thumbnailInfo.onGetflvLoad', info);
        if (!info.url) {
          thumbnailInfo.dispatchAsync(
            'onThumbnailInfoLoad',
            {status: 'ng', message: 'サムネイル情報の取得に失敗しました'}
            );
          return;
        } else
        if (info.url.indexOf('http://') !== 0) { // rtmpe:~など
          thumbnailInfo.dispatchAsync(
            'onThumbnailInfoLoad',
            {status: 'ng', message: 'この配信形式には対応していません'}
            );
          return;
        }

        var url = info.url + '&sb=1';

        thumbnailInfo.dispatchEvent('onThumbnailInfoLoadStart');
        if (cache[url]) {
          //console.log('%cthumbnailInfo cache exist', 'background: cyan', url);
          thumbnailInfo.dispatchAsync('onThumbnailInfoLoad', cache[url]);
          return;
        }
        loaderWindow.location.replace(url);
      };

      var onMessage = function(data, type) {
        if (type !== 'storyboard') { return; }
        //console.log('thumbnailInfo.onMessage: ', data, type);

        var url = data.url;
        var xml = data.xml, $xml = $(xml), $storyboard = $xml.find('storyboard');

        if ($storyboard.length < 1) {
          thumbnailInfo.dispatchAsync(
            'onThumbnailInfoLoad',
            {status: 'ng', message: 'この動画にはサムネイルがありません'}
            );
          return;
        }
        var info = {
          status:   'ok',
          message:  '成功',
          url:      data.url,
          movieId:  $xml.find('movie').attr('id'),
          duration: $xml.find('duration').text(),
          thumbnail:{
            width:    $storyboard.find('thumbnail_width').text(),
            height:   $storyboard.find('thumbnail_height').text(),
            number:   $storyboard.find('thumbnail_number').text(),
            interval: $storyboard.find('thumbnail_interval').text()
          },
          board: {
            rows:   $storyboard.find('board_rows').text(),
            cols:   $storyboard.find('board_cols').text(),
            number: $storyboard.find('board_number').text()
          }
        };

        cache[url] = info;
        thumbnailInfo.dispatchAsync('onThumbnailInfoLoad', info);
      };

      var initialize = function() {
        initialize = _.noop;

        console.log('%c initialize thumbnailInfo', 'background: lightgreen;');

        loaderFrame = document.createElement('iframe');
        loaderFrame.name      = 'StoryboardLoader';
        loaderFrame.className = DEBUG ? 'xDomainLoaderFrame debug' : 'xDomainLoaderFrame';
        document.body.appendChild(loaderFrame);

        loaderWindow = loaderFrame.contentWindow;

        eventDispatcher.addEventListener('onMessage', onMessage);
        getflv.addEventListener('onGetflvLoad', onGetflvLoad);
      };

      var load = function(watchId) {
        initialize();
        getflv.load(watchId);
      };


      window.WatchApp.mixin(thumbnailInfo, {
        load: load
      });

      return thumbnailInfo;
    })();

    window.NicovideoStoryboard.model.StoryboardModel = (function() {

      function StoryboardModel(params) {
        this._thumbnailInfo   = params.thumbnailInfo;
        this._isEnabled       = params.isEnabled;
        this._watchId         = params.watchId;

        window.WatchApp.extend(this, StoryboardModel, EventDispatcher);
      }

      window.WatchApp.mixin(StoryboardModel.prototype, {
        initialize: function(info) {
          console.log('%c initialize StoryboardModel', 'background: lightgreen;');

          this.update(info);
        },
        update: function(info) {
          if (info.status !== 'ok') {
            window.WatchApp.mixin(info, {
              url: '',
              width: 1,
              height: 1,
              duration: 1,
              thumbnail: {
                width: 1,
                height: 1,
                number: 1,
                interval: 1
              },
              board: {
                rows: 1,
                cols: 1
              }
            });
          }
          this._info = info;

          this.dispatchEvent('update');
        },

        reset: function() {
          if (this.isEnabled()) {
            window.setTimeout($.proxy(function() {
              this.load();
            }, this), 1000);
          }
          this.dispatchEvent('reset');
        },

        load: function() {
          this._isEnabled = true;
          this._thumbnailInfo.load(this._watchId);
        },

        setWatchId: function(watchId) {
          this._watchId = watchId;
        },

        unload: function() {
          this._isEnabled = false;
          this.dispatchEvent('unload');
        },

        isEnabled: function() {
          return this._isEnabled;
        },

        getStatus:   function() { return this._info.status; },
        getMessage:  function() { return this._info.message; },
        getUrl:      function() { return this._info.url; },
        getDuration: function() { return parseInt(this._info.duration, 10); },

        getWidth:    function() { return parseInt(this._info.thumbnail.width, 10); },
        getHeight:   function() { return parseInt(this._info.thumbnail.height, 10); },
        getInterval: function() { return parseInt(this._info.thumbnail.interval, 10); },
        getCount:    function() {
          return Math.max(
            Math.ceil(this.getDuration() / Math.max(0.01, this.getInterval())),
            parseInt(this._info.thumbnail.number, 10)
          );
        },
        getRows:   function() { return parseInt(this._info.board.rows, 10); },
        getCols:   function() { return parseInt(this._info.board.cols, 10); },
        getPageCount: function() { return parseInt(this._info.board.number, 10); },
        getTotalRows: function() {
          return Math.ceil(this.getCount() / this.getCols());
        },

        getPageWidth:    function() { return this.getWidth()  * this.getCols(); },
        getPageHeight:   function() { return this.getHeight() * this.getRows(); },
        getCountPerPage: function() { return this.getRows()   * this.getCols(); },

        /**
         *  nページ目のURLを返す。 ゼロオリジン
         */
        getPageUrl: function(page) {
          page = Math.max(0, Math.min(this.getPageCount() - 1, page));
          return this.getUrl() + '&board=' + (page + 1);
        },

        /**
         * vposに相当するサムネは何番目か?を返す
         */
        getIndex: function(vpos) {
          // msec -> sec
          var v = Math.floor(vpos / 1000);
          v = Math.max(0, Math.min(this.getDuration(), v));

          // サムネの総数 ÷ 秒数
          // Math.maxはゼロ除算対策
          var n = this.getCount() / Math.max(1, this.getDuration());

          return parseInt(Math.floor(v * n), 10);
        },

        /**
         * Indexのサムネイルは何番目のページにあるか?を返す
         */
        getPageIndex: function(thumbnailIndex) {
          var perPage   = this.getCountPerPage();
          var pageIndex = parseInt(thumbnailIndex / perPage, 10);
          return Math.max(0, Math.min(this.getPageCount(), pageIndex));
        },

        /**
         *  vposに相当するサムネは何ページの何番目にあるか?を返す
         */
        getThumbnailPosition: function(vpos) {
          var thumbnailIndex = this.getIndex(vpos);
          var pageIndex      = this.getPageIndex(thumbnailIndex);

          var mod = thumbnailIndex % this.getCountPerPage();
          var row = Math.floor(mod / Math.max(1, this.getCols()));
          var col = mod % this.getRows();

          return {
            page: pageIndex,
            index: thumbnailIndex,
            row: row,
            col: col
          };
        },

        /**
         * nページ目のx, y座標をvposに変換して返す
         */
        getPointVpos: function(x, y, page) {
          var width  = Math.max(1, this.getWidth());
          var height = Math.max(1, this.getHeight());
          var row = Math.floor(y / height);
          var col = Math.floor(x / width);
          var mod = x % width;


          // 何番目のサムネに相当するか?
          var point =
            page * this.getCountPerPage() +
            row  * this.getCols()         +
            col +
            (mod / width) // 小数点以下は、n番目の左端から何%あたりか
            ;

          // 全体の何%あたり?
          var percent = point / Math.max(1, this.getCount());
          percent = Math.max(0, Math.min(100, percent));

          // vposは㍉秒単位なので1000倍
          return Math.floor(this.getDuration() * percent * 1000);
        },

        /**
         * vposは何ページ目に当たるか?を返す
         */
        getVposPage: function(vpos) {
          var index = this._storyboard.getIndex(vpos);
          var page  = this._storyboard.getPageIndex(index);

          return page;
        }

      });

      return StoryboardModel;
    })();

    window.NicovideoStoryboard.view.FullScreenModeView = (function() {
      var __TEMPLATE__ = (function() {/*
        body.full_with_browser{
          background: #000;
        }
        body.full_with_browser.NicovideoStoryboardOpen #content{
          margin-bottom: {$storyboardHeight}px;
          transition: margin-bottom 0.5s ease-in-out;
        }
      */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');

      var addStyle = function(styles, id) {
        var elm = document.createElement('style');
        window.setTimeout(function() {
          elm.type = 'text/css';
          if (id) { elm.id = id; }

          var text = styles.toString();
          text = document.createTextNode(text);
          elm.appendChild(text);
          var head = document.getElementsByTagName('head');
          head = head[0];
          head.appendChild(elm);
        }, 0);
        return elm;
      };

      function FullScreenModeView() {

        this._css = null;
        this._lastHeight = -1;
      }

      window.WatchApp.mixin(FullScreenModeView.prototype, {
        initialize: function() {
          if (this._css) { return; }

          console.log('%cinitialize NicovideoStorybaordFullScreenStyle', 'background: lightgreen;');
          this._css = addStyle('/* undefined */', 'NicovideoStorybaordFullScreenStyle');
        },
        update: function($container) {
          this.initialize();

          var height = $container.outerHeight();

          if (height === this._lastHeight) { return; }
          this._lastHeight = height;

          var newCss = __TEMPLATE__.replace('{$storyboardHeight}', height);
          this._css.innerHTML = newCss;
        }
      });


      return FullScreenModeView;
    })();

    window.NicovideoStoryboard.view.SetToEnableButtonView = (function() {

      var TEXT = {
        DEFAULT:   'サムネイルを開く ▲',
        LOADING:   '動画を読み込み中...',
        GETFLV:    '動画情報を読み込み中...',
        THUMBNAIL: 'サムネイル情報を読み込み中...'
      };


      function SetToEnableButtonView(params) {
        this._storyboard      = params.storyboard;
        this._eventDispatcher = params.eventDispatcher;

        this.initialize();
      }

      window.WatchApp.mixin(SetToEnableButtonView.prototype, {
        initialize: function() {
          console.log('%cinitialize SetToEnableButtonView', 'background: lightgreen;');
          this._$view = $([
              '<div class="setToEnableButtonContainer loadingVideo">',
                '<button>', TEXT.DEFAULT, '</button>',
              '</div>',
              '',
              '',
            ''].join(''));
          this._$button = this._$view.find('button');
          this._$button.on('click', $.proxy(this._onButtonClick, this));

          var sb = this._storyboard;
          sb.addEventListener('reset',  $.proxy(this._onStoryboardReset, this));
          sb.addEventListener('update', $.proxy(this._onStoryboardUpdate, this));

          var evt = this._eventDispatcher;
          evt.addEventListener('getFlvLoadStart',
            $.proxy(this._onGetflvLoadStart, this));
          evt.addEventListener('onThumbnailInfoLoadStart',
            $.proxy(this._onThumbnailInfoLoadStart, this));
          evt.addEventListener('onWatchInfoReset',
            $.proxy(this._onWatchInfoReset, this));

          $('body').append(this._$view);
        },
        reset: function() {
          this._$view.attr('title', '');
          if (this._storyboard.isEnabled()) {
            this._$view.removeClass('loadingVideo getflv thumbnailInfo fail success').addClass('loading');
            this._setText(TEXT.GETFLV);
          } else {
            this._$view.removeClass('loadingVideo getflv thumbnailInfo fail success loading');
            this._setText(TEXT.DEFAULT);
          }
        },
        _setText: function(text) {
          this._$button.text(text);
        },
        _onButtonClick: function(e) {
          if (
            this._$view.hasClass('loading') ||
            this._$view.hasClass('loadingVideo') ||
            this._$view.hasClass('fail')) {
            return;
          }
          e.preventDefault();
          e.stopPropagation();

          var $view = this._$view.addClass('loading clicked');
          this._eventDispatcher.dispatchEvent('onEnableStoryboard');
          window.setTimeout(function() {
            $view.removeClass('clicked');
            $view = null;
          }, 1000);
        },
        _onStoryboardReset: function() {
          this.reset();
        },
        _onStoryboardUpdate: function() {
          var storyboard = this._storyboard;

          if (storyboard.getStatus() === 'ok') {
            window.setTimeout($.proxy(function() {
              this._$view
                .removeClass('loading getflv thumbnailInfo')
                .addClass('success')
                .attr('title', '');
              this._setText(TEXT.DEFAULT);
            }, this), 3000);
          } else {
            this._$view
              .removeClass('loading')
              .addClass('fail')
              .attr('title', DEBUG ? noThumbnailAA : '');
            this._setText(storyboard.getMessage());
          }
        },
        _onGetflvLoadStart: function() {
          this._$view.addClass('loading getflv');
          this._setText(TEXT.GETFLV);
        },
        _onThumbnailInfoLoadStart: function() {
          this._$view.addClass('loading thumbnailInfo');
          this._setText(TEXT.THUMBNAIL);
        },
        _onWatchInfoReset: function() {
          this._$view.addClass('loadingVideo');
          this._setText(TEXT.LOADING);
        }
      });

      return SetToEnableButtonView;
    })();

    window.NicovideoStoryboard.view.StoryboardView = (function() {
      var TIMER_INTERVAL = 33;
      var VPOS_RATE = 10;

      function StoryboardView(params) {
        this.initialize(params);
      }

      window.WatchApp.mixin(StoryboardView.prototype, {
        initialize: function(params) {
          console.log('%c initialize StoryboardView', 'background: lightgreen;');

          this._watchController = params.watchController;
          var evt = this._eventDispatcher = params.eventDispatcher;
          var sb  = this._storyboard = params.storyboard;

          this._isHover = false;
          this._currentUrl = '';
          this._lazyImage = {};
          this._lastPage = -1;
          this._lastVpos = 0;
          this._lastGetVpos = 0;
          this._timerCount = 0;
          this._scrollLeft = 0;

          this._enableButtonView =
            new window.NicovideoStoryboard.view.SetToEnableButtonView({
              storyboard:      sb,
              eventDispatcher: this._eventDispatcher
            });

          this._fullScreenModeView =
            new window.NicovideoStoryboard.view.FullScreenModeView();

          evt.addEventListener('onWatchInfoReset', $.proxy(this._onWatchInfoReset, this));

          sb.addEventListener('update', $.proxy(this._onStoryboardUpdate, this));
          sb.addEventListener('reset',  $.proxy(this._onStoryboardReset,  this));
          sb.addEventListener('unload', $.proxy(this._onStoryboardUnload, this));
        },
        _initializeStoryboard: function() {
          this._initializeStoryboard = _.noop;
          console.log('%cStoryboardView.initializeStoryboard', 'background: lightgreen;');

          var $view = this._$view = $(storyboardTemplate);

          var $inner = this._$inner = $view.find('.storyboardInner');
          this._$failMessage   = $view.find('.failMessage');
          this._$cursorTime    = $view.find('.cursorTime');
          this._$disableButton = $view.find('.setToDisable button');

          var self = this;
          var onHoverIn  = function() { self._isHover = true;  };
          var onHoverOut = function() { self._isHover = false; };
          $view
            .on('click',     '.board', $.proxy(this._onBoardClick,     this))
            .on('mousemove', '.board', $.proxy(this._onBoardMouseMove, this))
            .on('mousewheel',          $.proxy(this._onMouseWheel, this));

          $inner
            .hover(onHoverIn, onHoverOut)
            .on('scroll', _.throttle(function() { self._onScroll(); }, 500));

          this._watchController
            .addEventListener('onVideoSeeked', $.proxy(this._onVideoSeeked, this));

          this._$disableButton.on('click',
            $.proxy(this._onDisableButtonClick, this));

          $('body').append($view);
        },
        _onBoardClick: function(e) {
          var $board = $(e.target), offset = $board.offset();
          var y = $board.attr('data-top') * 1;
          var x = e.pageX - offset.left;
          var page = $board.attr('data-page');
          var vpos = this._storyboard.getPointVpos(x, y, page);
          if (isNaN(vpos)) { return; }

          var $view = this._$view;
          $view.addClass('clicked');
          window.setTimeout(function() { $view.removeClass('clicked'); }, 300);
          this._eventDispatcher.dispatchEvent('onStoryboardSelect', vpos);

          this._isHover = false;
          if ($board.hasClass('lazyImage')) { this._lazyLoadImage(page); }
        },
        _onBoardMouseMove: function(e) {
          var $board = $(e.target), offset = $board.offset();
          var y = $board.attr('data-top') * 1;
          var x = e.pageX - offset.left;
          var page = $board.attr('data-page');
          var vpos = this._storyboard.getPointVpos(x, y, page);
          if (isNaN(vpos)) { return; }
          var sec = Math.floor(vpos / 1000);

          var time = Math.floor(sec / 60) + ':' + ((sec % 60) + 100).toString().substr(1);
          this._$cursorTime.text(time).css({left: e.pageX});

          this._isHover = true;
          if ($board.hasClass('lazyImage')) { this._lazyLoadImage(page); }
        },
        _onMouseWheel: function(e, delta) {
          e.preventDefault();
          e.stopPropagation();
          this._isHover = true;
          var left = this.scrollLeft();
          this.scrollLeft(left - delta * 140);
        },
        _onVideoSeeked: function() {
          if (!this._storyboard.isEnabled()) {
            return;
          }
          if (this._storyboard.getStatus() !== 'ok') {
            return;
          }
          var vpos  = this._watchController.getVpos();
          var page = this._storyboard.getVposPage(vpos);

          this._lazyLoadImage(page);
          if (this.isHover || !this._watchController.isPlaying()) {
            this._onVposUpdate(vpos, true);
          } else {
            this._onVposUpdate(vpos);
          }
        },
        update: function() {
          this.disableTimer();

          this._initializeStoryboard();
          this._$view.removeClass('show success');
          $('body').removeClass('NicovideoStoryboardOpen');
          if (this._storyboard.getStatus() === 'ok') {
            this._updateSuccess();
          } else {
            this._updateFail();
          }
        },
        scrollLeft: function(left) {
          if (left === undefined) {
            return this._scrollLeft;
          } else
          if (left === 0 || Math.abs(this._scrollLeft - left) >= 1) {
            this._$inner[0].scrollLeft = left;
            this._scrollLeft = left;
          }
        },
        _updateSuccess: function() {
          var url = this._storyboard.getUrl();
          if (this._currentUrl === url) {
            this._$view.addClass('show success');
            this.enableTimer();
          } else {
            this._currentUrl = url;
            this._updateSuccessFull();
          }
          $('body').addClass('NicovideoStoryboardOpen');
        },
        _updateSuccessFull: function() {
          var storyboard = this._storyboard;
          var pages      = storyboard.getPageCount();
          var pageWidth  = storyboard.getPageWidth();
          var height     = storyboard.getHeight();
          var rows       = storyboard.getRows();

          var $borders =
            this._createBorders(storyboard.getWidth(), storyboard.getHeight(), storyboard.getCols());

          var totalRows = storyboard.getTotalRows();
          var rowCnt = 0;
          var $list = $('<div class="boardList"/>')
            .css({
              width: storyboard.getCount() * storyboard.getWidth(),
              paddingLeft: '50%',
              paddingRight: '50%',
              height: height
            });

          for (var i = 0; i < pages; i++) {
            var src = storyboard.getPageUrl(i);
            for (var j = 0; j < rows; j++) {
              var $img =
                $('<div class="board"/>')
                  .css({
                    width: pageWidth,
                    height: height,
                    backgroundPosition: '0 -' + height * j + 'px'
                  })
                  .attr({
                    'data-src': src,
                    'data-page': i,
                    'data-top': height * j + height / 2
                  })
                  .append($borders.clone());

              if (i === 0) { // 1ページ目だけ遅延ロードしない
                $img.css('background-image', 'url(' + src + ')');
              } else {
                $img.addClass('lazyImage page-' + i);
              }
              $list.append($img);
              rowCnt++;
              if (rowCnt >= totalRows) {
                break;
              }
            }
          }

          this._$innerList = $list;

          this._$inner.empty().append($list).append(this._$pointer);
          this._$view.removeClass('fail').addClass('success');

          this._fullScreenModeView.update(this._$view);

          window.setTimeout($.proxy(function() {
            this._$view.addClass('show');
          }, this), 100);

          this.scrollLeft(0);
          this.enableTimer();
        },
        _createBorders: function(width, height, count) {
          var $border = $('<div class="border"/>').css({
            width: width,
            height: height
          });
          var $div = $('<div />');
          for (var i = 0; i < count; i++) {
            $div.append($border.clone());
          }
          return $div;
        },
        _lazyLoadImage: function(pageNumber) {
          var className = 'page-' + pageNumber;

          if (pageNumber < 1 || this._lazyImage[className]) {
            return;
          }

          var src = this._storyboard.getPageUrl(pageNumber);
          this._lazyImage[className] = src;

          //console.log('%c set lazyLoadImage', 'background: cyan;', 'page: ' + pageNumber, '  url: ' + src);

          var load = $.proxy(function() {
            this._$inner.find('.' + className)
              .css('background-image', 'url(' + src + ')')
              .removeClass('lazyImage ' + className);
          }, this);

          window.setTimeout(load, 0);
          //window.setTimeout(load, 1000);
        },
        _updateFail: function() {
          this._$view.removeClass('success').addClass('fail');
          this.disableTimer();
        },
        clear: function() {
          if (this._$view) {
            this._$inner.empty();
          }
          this.disableTimer();
        },
        _clearTimer: function() {
          if (this._timer) {
            window.clearInterval(this._timer);
            this._timer = null;
          }
        },
        enableTimer: function() {
          this._clearTimer();
          this._isHover = false;
          this._timer = window.setInterval($.proxy(this._onTimerInterval, this), TIMER_INTERVAL);
        },
        disableTimer: function() {
          this._clearTimer();
        },
        _onTimerInterval: function() {
          if (this._isHover) { return; }
          if (!this._storyboard.isEnabled()) { return; }

          var div = VPOS_RATE;
          var mod = this._timerCount % div;
          this._timerCount++;

          var vpos;

          if (!this._watchController.isPlaying()) {
            return;
          }

          //  getVposが意外に時間を取るので回数を減らす
          // そもそもコメントパネルがgetVpos叩きまくってるんですがそれは
          if (mod === 0) {
            vpos = this._watchController.getVpos();
          } else {
            vpos = this._lastVpos;
          }

          this._onVposUpdate(vpos);
        },
        _onVposUpdate: function(vpos, isImmediately) {
          var storyboard = this._storyboard;
          var duration = Math.max(1, storyboard.getDuration());
          var per = vpos / (duration * 1000);
          var width = storyboard.getWidth();
          var boardWidth = storyboard.getCount() * width;
          var targetLeft = boardWidth * per + width * 0.4;
          var currentLeft = this.scrollLeft();
          var leftDiff = targetLeft - currentLeft;

          if (Math.abs(leftDiff) > 5000) {
            leftDiff = leftDiff * 0.93; // 大きくシークした時
          } else {
            leftDiff = leftDiff / VPOS_RATE;
          }

          this._lastVpos = vpos;

          this.scrollLeft(isImmediately ? targetLeft : (currentLeft + Math.round(leftDiff)));

        },
        _onScroll: function() {
          var storyboard = this._storyboard;
          var scrollLeft = this.scrollLeft();
          var page = Math.round(scrollLeft / (storyboard.getPageWidth() * storyboard.getRows()));
          this._lazyLoadImage(Math.min(page, storyboard.getPageCount() - 1));
        },
        reset: function() {
          this._lastVpos = -1;
          this._lastPage = -1;
          this._currentUrl = '';
          this._timerCount = 0;
          this._scrollLeft = 0;
          this._lazyImage = {};
          if (this._$view) {
            $('body').removeClass('NicovideoStoryboardOpen');
            this._$view.removeClass('show');
            this._$inner.empty();
          }
        },
        _onDisableButtonClick: function(e) {
          e.preventDefault();
          e.stopPropagation();

          var $button = this._$disableButton;
          $button.addClass('clicked');
          window.setTimeout(function() {
            $button.removeClass('clicked');
          }, 1000);

          this._eventDispatcher.dispatchEvent('onDisableStoryboard');
        },
        _onStoryboardUpdate: function() {
          this.update();
        },
        _onStoryboardReset:  function() {
        },
        _onStoryboardUnload: function() {
          $('body').removeClass('NicovideoStoryboardOpen');
          if (this._$view) {
            this._$view.removeClass('show');
          }
        },
        _onWatchInfoReset:  function() {
          this.reset();
        }
      });

      return StoryboardView;
    })();

    window.NicovideoStoryboard.controller.StoryboardController = (function() {

      function StoryboardController(params) {
        this.initialize(params);
      }

      window.WatchApp.mixin(StoryboardController.prototype, {
        initialize: function(params) {
          console.log('%c initialize StoryboardController', 'background: lightgreen;');

          this._thumbnailInfo   = params.thumbnailInfo;
          this._watchController = params.watchController;
          this._config          = params.config;

          var evt = this._eventDispatcher = params.eventDispatcher;

          evt.addEventListener('onVideoInitialized',
            $.proxy(this._onVideoInitialized, this));

          evt.addEventListener('onWatchInfoReset',
            $.proxy(this._onWatchInfoReset, this));

          evt.addEventListener('onStoryboardSelect',
            $.proxy(this._onStoryboardSelect, this));

          evt.addEventListener('onEnableStoryboard',
            $.proxy(this._onEnableStoryboard, this));

          evt.addEventListener('onDisableStoryboard',
            $.proxy(this._onDisableStoryboard, this));

          evt.addEventListener('onGetflvLoadStart',
            $.proxy(this._onGetflvLoadStart, this));

          evt.addEventListener('onThumbnailInfoLoadStart',
            $.proxy(this._onThumbnailInfoLoadStart, this));

          evt.addEventListener('onThumbnailInfoLoad',
            $.proxy(this._onThumbnailInfoLoad, this));

          this._initializeStoryboard();
        },

        _initializeStoryboard: function() {
          this._initializeStoryboard = _.noop;

          if (!this._storyboardModel) {
            var nsv = window.NicovideoStoryboard;
            this._storyboardModel = new nsv.model.StoryboardModel({
              thumbnailInfo:   this._thumbnailInfo,
              isEnabled:       this._config.get('enabled') === true,
              watchId:         this._watchController.getWatchId()
            });
          }
          if (!this._storyboardView) {
            this._storyboardView = new window.NicovideoStoryboard.view.StoryboardView({
              watchController: this._watchController,
              eventDispatcher: this._eventDispatcher,
              storyboard: this._storyboardModel
            });
          }
        },

        load: function(watchId) {
          if (watchId) {
            this._storyboardModel.setWatchId(watchId);
          }
          this._storyboardModel.load();
        },

        unload: function() {
          if (this._storyboardModel) {
            this._storyboardModel.unload();
          }
        },

        _onVideoInitialized: function() {
          this._initializeStoryboard();
          this._storyboardModel.reset();
        },

        _onWatchInfoReset: function() {
          this._storyboardModel.setWatchId(this._watchController.getWatchId());
        },

        _onThumbnailInfoLoad: function(info) {
          //console.log('StoryboardController._onThumbnailInfoLoad', info);

          this._storyboardModel.update(info);
        },

        _onStoryboardSelect: function(vpos) {
          //console.log('_onStoryboardSelect', vpos);
          this._watchController.setVpos(vpos);
        },

        _onEnableStoryboard: function() {
          window.setTimeout($.proxy(function() {
            this._config.set('enabled', true);
            this.load();
          }, this), 0);
        },

        _onDisableStoryboard: function() {
          window.setTimeout($.proxy(function() {
            this._config.set('enabled', false);
            this.unload();
          }, this), 0);
        },

        _onGetflvLoadStart: function() {
        },

        _onThumbnailInfoLoadStart: function() {
        }
      });

      return StoryboardController;
    })();


    window.WatchApp.mixin(window.NicovideoStoryboard, {
      _addStyle: function(styles, id) {
        var elm = document.createElement('style');
        window.setTimeout(function() {
          elm.type = 'text/css';
          if (id) { elm.id = id; }

          var text = styles.toString();
          text = document.createTextNode(text);
          elm.appendChild(text);
          var head = document.getElementsByTagName('head');
          head = head[0];
          head.appendChild(elm);
        }, 0);
        return elm;
      },
      initialize: function() {
        console.log('%c initialize NicovideoStoryboard', 'background: lightgreen;');
        this.initializeUserConfig();

        var root = window.WatchApp.ns;
        this._playerAreaConnector =
          root.init.PlayerInitializer.playerAreaConnector;
        this._watchInfoModel =
          root.model.WatchInfoModel.getInstance();

        this._getflv          = window.NicovideoStoryboard.api.getflv;
        this._thumbnailInfo   = window.NicovideoStoryboard.api.thumbnailInfo;
        this._watchController = window.NicovideoStoryboard.external.watchController;

        this._eventDispatcher = new EventDispatcher();

        if (!this._watchController.isPremium()) {
          this._watchController.popup.alert('プレミアムの機能を使っているため、一般では動きません');
          return;
        }

        this._storyboardController = new window.NicovideoStoryboard.controller.StoryboardController({
          thumbnailInfo:       this._thumbnailInfo,
          watchController:     this._watchController,
          eventDispatcher:     this._eventDispatcher,
          config:              this.config
        });

        this.initializeEvent();

        this._addStyle(__css__, 'NicovideoStoryboardCss');
      },
      initializeEvent: function() {
        console.log('%c initializeEvent NicovideoStoryboard', 'background: lightgreen;');

        var eventDispatcher = this._eventDispatcher;

        this._watchInfoModel.addEventListener('reset', function() {
          eventDispatcher.dispatchEvent('onWatchInfoReset');
        });

        this._playerAreaConnector.addEventListener('onVideoInitialized', function() {
          eventDispatcher.dispatchEvent('onVideoInitialized');
        });

        this._getflv.addEventListener('onGetflvLoadStart', function() {
          eventDispatcher.dispatchEvent('onGetflvLoadStart');
        });
        this._getflv.addEventListener('onGetflvLoad', function(info) {
          eventDispatcher.dispatchEvent('onGetflvLoad', info);
        });

        this._thumbnailInfo.addEventListener('onThumbnailInfoLoadStart', function() {
          eventDispatcher.dispatchEvent('onThumbnailInfoLoadStart');
        });
        this._thumbnailInfo.addEventListener('onThumbnailInfoLoad', function(info) {
          eventDispatcher.dispatchEvent('onThumbnailInfoLoad', info);
        });

      },
      initializeUserConfig: function() {
        var prefix = 'NicoStoryboard_';
        var conf = {
          enabled: true,
          autoScroll: true
        };

        this.config = {
          get: function(key) {
            try {
              if (window.localStorage.hasOwnProperty(prefix + key)) {
                return JSON.parse(window.localStorage.getItem(prefix + key));
              }
              return conf[key];
            } catch (e) {
              return conf[key];
            }
          },
          set: function(key, value) {
            window.localStorage.setItem(prefix + key, JSON.stringify(value));
          }
        };
      },
      load: function(watchId) {
        // 動画ごとのcookieがないと取得できないので指定できてもあまり意味は無い
        watchId = watchId || this._watchController.getWatchId();
        this._storyboardController.load(watchId);
      }

    });


    //======================================
    //======================================
    //======================================

    (function() {
      var watchInfoModel = window.WatchApp.ns.model.WatchInfoModel.getInstance();
      if (watchInfoModel.initialized) {
        console.log('%c initialize', 'background: lightgreen;');
        window.NicovideoStoryboard.initialize();
      } else {
        var onReset = function() {
          watchInfoModel.removeEventListener('reset', onReset);
          window.setTimeout(function() {
            watchInfoModel.removeEventListener('reset', onReset);
            console.log('%c initialize', 'background: lightgreen;');
            window.NicovideoStoryboard.initialize();
          }, 0);
        };
        watchInfoModel.addEventListener('reset', onReset);
      }
    })();

  };

  var flapi = function() {
    if (window.name.indexOf('getflvLoader') < 0 ) { return; }

    var resp    = document.documentElement.textContent;
    var origin  = 'http://' + location.host.replace(/^.*?\./, 'www.');

    try {
      parent.postMessage(JSON.stringify({
          id: 'NicovideoStoryboard',
          type: 'getflv',
          body: {
            url: location.href,
            info: resp
          }
        }),
        origin);
    } catch (e) {
      alert(e);
      console.log('err', e);
    }
  };


  var smileapi = function() {
    if (window.name.indexOf('StoryboardLoader') < 0 ) { return; }

    var resp    = document.getElementsByTagName('smile');
    var origin  = 'http://' + location.host.replace(/^.*?\./, 'www.');
    var xml = '';

    if (resp.length > 0) {
      xml = resp[0].outerHTML;
    }

    try {
      parent.postMessage(JSON.stringify({
          id: 'NicovideoStoryboard',
          type: 'storyboard',
          body: {
            url: location.href,
            xml: xml
          }
        }),
        origin);
    } catch (e) {
      console.log('err', e);
    }
  };



  var host = window.location.host || '';
  if (host === 'flapi.nicovideo.jp') {
    flapi();
  } else
  if (host.indexOf('smile-') >= 0) {
    smileapi();
  } else {
    var script = document.createElement('script');
    script.id = 'NicoVideoStoryboard';
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('charset', 'UTF-8');
    script.appendChild(document.createTextNode("(" + monkey + ")()"));
    document.body.appendChild(script);
  }

})();