NicovideoStoryboard

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

目前為 2014-05-08 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);
  }

})();