NicovideoStoryboard

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

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

  1. // ==UserScript==
  2. // @name NicovideoStoryboard
  3. // @namespace https://github.com/segabito/
  4. // @description シークバーに出るサムネイルを並べて表示
  5. // @match http://www.nicovideo.jp/watch/*
  6. // @match http://flapi.nicovideo.jp/api/getflv*
  7. // @match http://*.nicovideo.jp/smile*
  8. // @grant none
  9. // @author segabito macmoto
  10. // @version 1.0.5
  11. // ==/UserScript==
  12.  
  13. // ver 1.0.5 デモモードを追加。連続再生時、サムネイルの無い動画をスキップする (iPadで見せびらかす用モード)
  14.  
  15. // ver 1.0.4 タッチパネルでの操作を改善 (Windows Chromeのみ。Firefoxはいまいち)
  16.  
  17. // ver 1.0.3 WUXGAモニターフルスクリーン時に動画部分が1920x1080になるよう調整
  18.  
  19. // ver 1.0.1 フルスクリーンモードで開いた時はプレイヤー領域を押し上げるようにした
  20.  
  21. (function() {
  22. var monkey = function() {
  23. var DEBUG = !true;
  24. var $ = window.jQuery, _ = window._;
  25.  
  26. var __css__ = (function() {/*
  27. .xDomainLoaderFrame {
  28. position: fixed;
  29. top: -999px;
  30. left: -999px;
  31. width: 1px;
  32. height: 1px;
  33. border: 0;
  34. }
  35.  
  36. .storyboardContainer {
  37. position: fixed;
  38. bottom: -300px;
  39. left: 0;
  40. right: 0;
  41. width: 100%;
  42. box-sizing: border-box;
  43. -moz-box-sizing: border-box;
  44. -webkit-box-sizing: border-box;
  45. background: #999;
  46. border: 2px outset #000;
  47. z-index: 9005;
  48. overflow: visible;
  49. border-radius: 10px 10px 0 0;
  50. border-width: 2px 2px 0;
  51. box-shadow: 0 -2px 2px #666;
  52.  
  53. transition: bottom 0.5s ease-in-out;
  54. }
  55.  
  56. .storyboardContainer.show {
  57. bottom: 0;
  58. }
  59.  
  60. .storyboardContainer .storyboardInner {
  61. display: none;
  62. position: relative;
  63. text-align: center;
  64. overflow-x: scroll;
  65. white-space: nowrap;
  66. background: #222;
  67. margin: 4px 12px 3px;
  68. border-style: inset;
  69. border-width: 2px 4px;
  70. border-radius: 10px 10px 0 0;
  71. }
  72.  
  73. .storyboardContainer.success .storyboardInner {
  74. display: block;
  75. }
  76.  
  77. .storyboardContainer .storyboardInner .boardList {
  78. overflow: hidden;
  79. }
  80.  
  81. .storyboardContainer .boardList .board {
  82. display: inline-block;
  83. cursor: pointer;
  84. background-color: #101010;
  85. }
  86.  
  87. .storyboardContainer.clicked .storyboardInner * {
  88. opacity: 0.3;
  89. pointer-events: none;
  90. }
  91.  
  92. .storyboardContainer.opening .storyboardInner .boardList .board {
  93. pointer-events: none;
  94. }
  95.  
  96. .storyboardContainer .boardList .board.lazyImage {
  97. background-color: #ccc;
  98. }
  99. .storyboardContainer .boardList .board.loadFail {
  100. background-color: #c99;
  101. }
  102. .storyboardContainer .boardList .board.lazyImage {
  103. cursor: wait;
  104. }
  105.  
  106. .storyboardContainer .boardList .board > div {
  107. white-space: nowrap;
  108. }
  109. .storyboardContainer .boardList .board .border {
  110. box-sizing: border-box;
  111. -moz-box-sizing: border-box;
  112. -webkit-box-sizing: border-box;
  113. border-style: solid;
  114. border-color: #000 #333 #000 #999;
  115. border-width: 0 2px 0 2px;
  116. display: inline-block;
  117. }
  118. .storyboardContainer .boardList .board:hover .border {
  119. display: none; {* クリックできなくなっちゃうので苦し紛れの対策 もうちょっとマシな方法を考える *}
  120. }
  121.  
  122. .storyboardContainer .storyboardHeader {
  123. position: relative;
  124. width: 100%;
  125. }
  126. .storyboardContainer .pointer {
  127. position: absolute;
  128. bottom: -15px;
  129. left: 50%;
  130. width: 32px;
  131. margin-left: -16px;
  132. color: #333;
  133. z-index: 9010;
  134. text-align: center;
  135. }
  136.  
  137. .storyboardContainer .cursorTime {
  138. display: none;
  139. position: absolute;
  140. bottom: -30px;
  141. left: -999px;
  142. font-size: 10pt;
  143. border: 1px solid #000;
  144. z-index: 9010;
  145. background: #ffc;
  146. pointer-events: none;
  147. }
  148. .storyboardContainer:hover .cursorTime {
  149. display: block;
  150. }
  151.  
  152. .storyboardContainer.clicked .cursorTime,
  153. .storyboardContainer.opening .cursorTime {
  154. display: none;
  155. }
  156.  
  157.  
  158. .storyboardContainer .setToDisable {
  159. position: absolute;
  160. display: inline-block;
  161. left: 250px;
  162. bottom: -32px;
  163. transition: bottom 0.3s ease-in-out;
  164. }
  165. .storyboardContainer:hover .setToDisable {
  166. bottom: 0;
  167. }
  168.  
  169. .storyboardContainer .setToDisable button,
  170. .setToEnableButtonContainer button {
  171. background: none repeat scroll 0 0 #999;
  172. border-color: #666;
  173. border-radius: 18px 18px 0 0;
  174. border-style: solid;
  175. border-width: 2px 2px 0;
  176. width: 200px;
  177. overflow: auto;
  178. white-space: nowrap;
  179. cursor: pointer;
  180. box-shadow: 0 -2px 2px #666;
  181. }
  182.  
  183. .full_with_browser .setToEnableButtonContainer button {
  184. box-shadow: none;
  185. color: #888;
  186. background: #000;
  187. }
  188.  
  189. .full_with_browser .storyboardContainer .setToDisable,
  190. .full_with_browser .setToEnableButtonContainer {
  191. background: #000; {* Firefox対策 *}
  192. }
  193.  
  194. .setToEnableButtonContainer button {
  195. width: 200px;
  196. }
  197.  
  198. .storyboardContainer .setToDisable button:hover,
  199. .setToEnableButtonContainer:not(.loading):not(.fail) button:hover {
  200. background: #ccc;
  201. transition: none;
  202. }
  203.  
  204. .storyboardContainer .setToDisable button.clicked,
  205. .setToEnableButtonContainer.loading button,
  206. .setToEnableButtonContainer.fail button,
  207. .setToEnableButtonContainer button.clicked {
  208. border-style: inset;
  209. box-shadow: none;
  210. }
  211.  
  212. .setToEnableButtonContainer {
  213. position: fixed;
  214. z-index: 9003;
  215. left: 250px;
  216. bottom: 0px;
  217. transition: bottom 0.5s ease-in-out;
  218. }
  219. .setToEnableButtonContainer.loadingVideo {
  220. bottom: -50px;
  221. }
  222.  
  223. .setToEnableButtonContainer.loading *,
  224. .setToEnableButtonContainer.loadingVideo *{
  225. cursor: wait;
  226. font-size: 80%;
  227. }
  228. .setToEnableButtonContainer.fail {
  229. color: #999;
  230. cursor: default;
  231. font-size: 80%;
  232. }
  233.  
  234. .NicoVideoStoryboardSettingMenu {
  235. height: 44px !important;
  236. }
  237. .NicoVideoStoryboardSettingMenu a {
  238. font-weight: bolder;
  239. }
  240. #NicoVideoStoryboardSettingPanel {
  241. position: fixed;
  242. bottom: 2000px; right: 8px;
  243. z-index: -1;
  244. width: 500px;
  245. background: #f0f0f0; border: 1px solid black;
  246. padding: 8px;
  247. transition: bottom 0.4s ease-out;
  248. text-align: left;
  249. }
  250. #NicoVideoStoryboardSettingPanel.open {
  251. display: block;
  252. bottom: 8px;
  253. box-shadow: 0 0 8px black;
  254. z-index: 10000;
  255. }
  256. #NicoVideoStoryboardSettingPanel .close {
  257. position: absolute;
  258. cursor: pointer;
  259. right: 8px; top: 8px;
  260. }
  261. #NicoVideoStoryboardSettingPanel .panelInner {
  262. background: #fff;
  263. border: 1px inset;
  264. padding: 8px;
  265. min-height: 300px;
  266. overflow-y: scroll;
  267. max-height: 500px;
  268. }
  269. #NicoVideoStoryboardSettingPanel .panelInner .item {
  270. border-bottom: 1px dotted #888;
  271. margin-bottom: 8px;
  272. padding-bottom: 8px;
  273. }
  274. #NicoVideoStoryboardSettingPanel .panelInner .item:hover {
  275. background: #eef;
  276. }
  277. #NicoVideoStoryboardSettingPanel .windowTitle {
  278. font-size: 150%;
  279. }
  280. #NicoVideoStoryboardSettingPanel .itemTitle {
  281. }
  282. #NicoVideoStoryboardSettingPanel label {
  283. margin-right: 12px;
  284. }
  285. #NicoVideoStoryboardSettingPanel small {
  286. color: #666;
  287. }
  288. #NicoVideoStoryboardSettingPanel .expert {
  289. margin: 32px 0 16px;
  290. font-size: 150%;
  291. background: #ccc;
  292. }
  293.  
  294.  
  295. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  296.  
  297. var storyboardTemplate = [
  298. '<div id="storyboardContainer" class="storyboardContainer">',
  299. '<div class="storyboardHeader">',
  300. '<div class="setToDisable"><button>閉じる ▼</button></div>',
  301. '<div class="pointer">▼</div>',
  302. '<div class="cursorTime"></div>',
  303. '</div>',
  304.  
  305. '<div class="storyboardInner"></div>',
  306. '<div class="failMessage">',
  307. '</div>',
  308. '</div>',
  309. '',
  310. ''].join('');
  311.  
  312. // マスコットキャラクターのサムネーヨちゃん
  313. var noThumbnailAA = (function() {/*
  314.  ∧ ∧     ┌─────────────
  315.  ( ´ー`)   < サムネーヨ
  316.  \ <     └───/|────────
  317.    \.\______//
  318.      \       /
  319.       ∪∪‾∪∪
  320. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  321.  
  322. var EventDispatcher = (function() {
  323.  
  324. function AsyncEventDispatcher() {
  325. window.WatchApp.extend(
  326. this,
  327. AsyncEventDispatcher,
  328. window.WatchApp.ns.event.EventDispatcher);
  329. }
  330.  
  331. window.WatchApp.mixin(AsyncEventDispatcher.prototype, {
  332. // addEventListener: function(name, func) {
  333. // console.log('%caddEventListener: ', 'background: red; color: white;', name, func);
  334. // if (!func) {
  335. // console.trace();
  336. // }
  337. // AsyncEventDispatcher.__super__.addEventListener.call(this, name, func);
  338. // },
  339. dispatchAsync: function() {
  340. var args = arguments;
  341.  
  342. window.setTimeout($.proxy(function() {
  343. try {
  344. this.dispatchEvent.apply(this, args);
  345. } catch (e) {
  346. console.log(e);
  347. }
  348. }, this), 0);
  349. }
  350. });
  351. return AsyncEventDispatcher;
  352. })();
  353.  
  354. window.NicovideoStoryboard =
  355. (function() {
  356. return {
  357. api: {},
  358. model: {},
  359. view: {},
  360. controller: {},
  361. external: {},
  362. event: {}
  363. };
  364. })();
  365.  
  366. // var console =
  367. // (function(debug) {
  368. // var n = window._.noop;
  369. // return debug ? window.console : {log: n, trace: n, time: n, timeEnd: n};
  370. // })(DEBUG);
  371. var console = window.console;
  372.  
  373. window.NicovideoStoryboard.event.windowEventDispatcher = (function() {
  374. var eventDispatcher = new EventDispatcher();
  375.  
  376. var onMessage = function(event) {
  377. if (event.origin.indexOf('nicovideo.jp') < 0) return;
  378. try {
  379. var data = JSON.parse(event.data);
  380. if (data.id !== 'NicovideoStoryboard') { return; }
  381.  
  382. eventDispatcher.dispatchEvent('onMessage', data.body, data.type);
  383. } catch (e) {
  384. console.log(
  385. '%cNicoVideoStoryboard.Error: window.onMessage - ',
  386. 'color: red; background: yellow',
  387. e
  388. );
  389. console.log('%corigin: ', 'background: yellow;', event.origin);
  390. console.log('%cdata: ', 'background: yellow;', event.data);
  391. console.trace();
  392. }
  393. };
  394.  
  395. window.addEventListener('message', onMessage);
  396.  
  397. return eventDispatcher;
  398. })();
  399.  
  400.  
  401. window.NicovideoStoryboard.external.watchController = (function() {
  402. var root = window.WatchApp.ns;
  403. var nicoPlayerConnector = root.init.PlayerInitializer.nicoPlayerConnector;
  404. var watchInfoModel = root.model.WatchInfoModel.getInstance();
  405. var viewerInfoModel = root.init.CommonModelInitializer.viewerInfoModel;
  406. var playerAreaConnector = root.init.PlayerInitializer.playerAreaConnector;
  407. var playlist = root.init.PlaylistInitializer.playlist;
  408. var externalNicoplayer;
  409.  
  410. var watchController = new EventDispatcher();
  411.  
  412. var getVpos = function() {
  413. return nicoPlayerConnector.getVpos();
  414. };
  415. var setVpos = function(vpos) {
  416. nicoPlayerConnector.seekVideo(vpos);
  417. };
  418.  
  419. var _isPlaying = null;
  420. var isPlaying = function() {
  421. if (_isPlaying !== null) {
  422. return _isPlaying;
  423. }
  424. if (!externalNicoplayer) {
  425. externalNicoplayer = $("#external_nicoplayer")[0];
  426. }
  427. var status = externalNicoplayer.ext_getStatus();
  428. return status === 'playing';
  429. };
  430. var play = function() {
  431. nicoPlayerConnector.playVideo();
  432. };
  433. var pause = function() {
  434. nicoPlayerConnector.stopVideo();
  435. };
  436.  
  437. var isPremium = function() {
  438. return !!viewerInfoModel.isPremium;
  439. };
  440.  
  441. var getWatchId = function() {// スレッドIDだったりsmXXXXだったり
  442. return watchInfoModel.v;
  443. };
  444. var getVideoId = function() {// smXXXXXX, soXXXXX など
  445. return watchInfoModel.id;
  446. };
  447.  
  448. var popupMarquee = root.init.PopupMarqueeInitializer.popupMarqueeViewController;
  449. var popup = {
  450. message: function(text) {
  451. popupMarquee.onData(
  452. '<span style="background: black;">' + text + '</span>'
  453. );
  454. },
  455. alert: function(text) {
  456. popupMarquee.onData(
  457. '<span style="background: black; color: red;">' + text + '</span>'
  458. );
  459. }
  460. };
  461.  
  462. var _playlist = {
  463. isContinuous: function() {
  464. return playlist.isContinuous();
  465. },
  466. playNext: function() {
  467. nicoPlayerConnector.playNextVideo();
  468. },
  469. playPrev: function() {
  470. nicoPlayerConnector.playpreviousVideo();
  471. }
  472. };
  473.  
  474. playerAreaConnector.addEventListener('onVideoPlayed', function() {
  475. _isPlaying = true;
  476. watchController.dispatchEvent('onVideoPlayed');
  477. });
  478. playerAreaConnector.addEventListener('onVideoStopped', function() {
  479. _isPlaying = false;
  480. watchController.dispatchEvent('onVideoStopped');
  481. });
  482.  
  483. playerAreaConnector.addEventListener('onVideoStarted', function() {
  484. _isPlaying = true;
  485. watchController.dispatchEvent('onVideoStarted');
  486. });
  487. playerAreaConnector.addEventListener('onVideoEnded', function() {
  488. _isPlaying = false;
  489. watchController.dispatchEvent('onVideoEnded');
  490. });
  491.  
  492. playerAreaConnector.addEventListener('onVideoSeeking', function() {
  493. //console.log('%conVideoSeeking', 'background: cyan');
  494. watchController.dispatchEvent('onVideoSeeking');
  495. });
  496. playerAreaConnector.addEventListener('onVideoSeeked', function() {
  497. //console.log('%conVideoSeeked', 'background: cyan');
  498. watchController.dispatchEvent('onVideoSeeked');
  499. });
  500.  
  501. playerAreaConnector.addEventListener('onVideoInitialized', function() {
  502. watchController.dispatchEvent('onVideoInitialized');
  503. });
  504.  
  505. watchInfoModel.addEventListener('reset', function() {
  506. watchController.dispatchEvent('onWatchInfoReset');
  507. });
  508.  
  509.  
  510.  
  511. window.WatchApp.mixin(watchController, {
  512. getVpos: getVpos,
  513. setVpos: setVpos,
  514.  
  515. isPlaying: isPlaying,
  516. play: play,
  517. pause: pause,
  518.  
  519. isPremium: isPremium,
  520.  
  521. getWatchId: getWatchId,
  522. getVideoId: getVideoId,
  523.  
  524. popup: popup,
  525. playlist: _playlist
  526. });
  527.  
  528. return watchController;
  529. })();
  530.  
  531.  
  532. window.NicovideoStoryboard.api.getflv = (function() {
  533. var BASE_URL = 'http://flapi.nicovideo.jp/api/getflv?v=';
  534. var loaderFrame, loaderWindow, cache = {};
  535. var eventDispatcher = window.NicovideoStoryboard.event.windowEventDispatcher;
  536. var getflv = new EventDispatcher();
  537.  
  538. var parseInfo = function(q) {
  539. var info = {}, lines = q.split('&');
  540. $.each(lines, function(i, line) {
  541. var tmp = line.split('=');
  542. var key = window.unescape(tmp[0]), value = window.unescape(tmp[1]);
  543. info[key] = value;
  544. });
  545. return info;
  546. };
  547.  
  548. var onMessage = function(data, type) {
  549. if (type !== 'getflv') { return; }
  550. var info = parseInfo(data.info), url = data.url;
  551.  
  552. cache[url] = info;
  553. //console.log('getflv.onGetflvLoad', info);
  554. getflv.dispatchAsync('onGetflvLoad', info);
  555. };
  556.  
  557. var initialize = function() {
  558. initialize = _.noop;
  559.  
  560. console.log('%c initialize getflv', 'background: lightgreen;');
  561.  
  562. loaderFrame = document.createElement('iframe');
  563. loaderFrame.name = 'getflvLoader';
  564. loaderFrame.className = DEBUG ? 'xDomainLoaderFrame debug' : 'xDomainLoaderFrame';
  565. document.body.appendChild(loaderFrame);
  566.  
  567. loaderWindow = loaderFrame.contentWindow;
  568.  
  569. eventDispatcher.addEventListener('onMessage', onMessage);
  570. };
  571.  
  572. var load = function(watchId) {
  573. initialize();
  574. var url = BASE_URL + watchId;
  575. //console.log('getflv: ', url);
  576.  
  577. getflv.dispatchEvent('onGetflvLoadStart', watchId);
  578. if (cache[url]) {
  579. //console.log('%cgetflv cache exist', 'background: cyan', url);
  580. getflv.dispatchAsync('onGetflvLoad', cache[url]);
  581. } else {
  582. loaderWindow.location.replace(url);
  583. }
  584. };
  585.  
  586. window.WatchApp.mixin(getflv, {
  587. load: load
  588. });
  589.  
  590. return getflv;
  591. })();
  592.  
  593. window.NicovideoStoryboard.api.thumbnailInfo = (function() {
  594. var getflv = window.NicovideoStoryboard.api.getflv;
  595. var loaderFrame, loaderWindow, cache = {};
  596. var eventDispatcher = window.NicovideoStoryboard.event.windowEventDispatcher;
  597. var thumbnailInfo = new EventDispatcher();
  598.  
  599. var onGetflvLoad = function(info) {
  600. //console.log('thumbnailInfo.onGetflvLoad', info);
  601. if (!info.url) {
  602. thumbnailInfo.dispatchAsync(
  603. 'onThumbnailInfoLoad',
  604. {status: 'ng', message: 'サムネイル情報の取得に失敗しました'}
  605. );
  606. return;
  607. } else
  608. if (info.url.indexOf('http://') !== 0) { // rtmpe:~など
  609. thumbnailInfo.dispatchAsync(
  610. 'onThumbnailInfoLoad',
  611. {status: 'ng', message: 'この配信形式には対応していません'}
  612. );
  613. return;
  614. }
  615.  
  616. var url = info.url + '&sb=1';
  617.  
  618. thumbnailInfo.dispatchEvent('onThumbnailInfoLoadStart');
  619. if (cache[url]) {
  620. //console.log('%cthumbnailInfo cache exist', 'background: cyan', url);
  621. thumbnailInfo.dispatchAsync('onThumbnailInfoLoad', cache[url]);
  622. return;
  623. }
  624. loaderWindow.location.replace(url);
  625. };
  626.  
  627. var onMessage = function(data, type) {
  628. if (type !== 'storyboard') { return; }
  629. //console.log('thumbnailInfo.onMessage: ', data, type);
  630.  
  631. var url = data.url;
  632. var xml = data.xml, $xml = $(xml), $storyboard = $xml.find('storyboard');
  633.  
  634. if ($storyboard.length < 1) {
  635. thumbnailInfo.dispatchAsync(
  636. 'onThumbnailInfoLoad',
  637. {status: 'ng', message: 'この動画にはサムネイルがありません'}
  638. );
  639. return;
  640. }
  641. var info = {
  642. status: 'ok',
  643. message: '成功',
  644. url: data.url,
  645. movieId: $xml.find('movie').attr('id'),
  646. duration: $xml.find('duration').text(),
  647. thumbnail:{
  648. width: $storyboard.find('thumbnail_width').text(),
  649. height: $storyboard.find('thumbnail_height').text(),
  650. number: $storyboard.find('thumbnail_number').text(),
  651. interval: $storyboard.find('thumbnail_interval').text()
  652. },
  653. board: {
  654. rows: $storyboard.find('board_rows').text(),
  655. cols: $storyboard.find('board_cols').text(),
  656. number: $storyboard.find('board_number').text()
  657. }
  658. };
  659.  
  660. cache[url] = info;
  661. thumbnailInfo.dispatchAsync('onThumbnailInfoLoad', info);
  662. };
  663.  
  664. var initialize = function() {
  665. initialize = _.noop;
  666.  
  667. console.log('%c initialize thumbnailInfo', 'background: lightgreen;');
  668.  
  669. loaderFrame = document.createElement('iframe');
  670. loaderFrame.name = 'StoryboardLoader';
  671. loaderFrame.className = DEBUG ? 'xDomainLoaderFrame debug' : 'xDomainLoaderFrame';
  672. document.body.appendChild(loaderFrame);
  673.  
  674. loaderWindow = loaderFrame.contentWindow;
  675.  
  676. eventDispatcher.addEventListener('onMessage', onMessage);
  677. getflv.addEventListener('onGetflvLoad', onGetflvLoad);
  678. };
  679.  
  680. var load = function(watchId) {
  681. initialize();
  682. getflv.load(watchId);
  683. };
  684.  
  685.  
  686. window.WatchApp.mixin(thumbnailInfo, {
  687. load: load
  688. });
  689.  
  690. return thumbnailInfo;
  691. })();
  692.  
  693. window.NicovideoStoryboard.model.StoryboardModel = (function() {
  694.  
  695. function StoryboardModel(params) {
  696. this._thumbnailInfo = params.thumbnailInfo;
  697. this._isEnabled = params.isEnabled;
  698. this._watchId = params.watchId;
  699.  
  700. window.WatchApp.extend(this, StoryboardModel, EventDispatcher);
  701. }
  702.  
  703. window.WatchApp.mixin(StoryboardModel.prototype, {
  704. initialize: function(info) {
  705. console.log('%c initialize StoryboardModel', 'background: lightgreen;');
  706.  
  707. this.update(info);
  708. },
  709. update: function(info) {
  710. if (info.status !== 'ok') {
  711. window.WatchApp.mixin(info, {
  712. url: '',
  713. width: 1,
  714. height: 1,
  715. duration: 1,
  716. thumbnail: {
  717. width: 1,
  718. height: 1,
  719. number: 1,
  720. interval: 1
  721. },
  722. board: {
  723. rows: 1,
  724. cols: 1
  725. }
  726. });
  727. }
  728. this._info = info;
  729.  
  730. this.dispatchEvent('update');
  731. },
  732.  
  733. reset: function() {
  734. if (this.isEnabled()) {
  735. window.setTimeout($.proxy(function() {
  736. this.load();
  737. }, this), 1000);
  738. }
  739. this.dispatchEvent('reset');
  740. },
  741.  
  742. load: function() {
  743. this._isEnabled = true;
  744. this._thumbnailInfo.load(this._watchId);
  745. },
  746.  
  747. setWatchId: function(watchId) {
  748. this._watchId = watchId;
  749. },
  750.  
  751. unload: function() {
  752. this._isEnabled = false;
  753. this.dispatchEvent('unload');
  754. },
  755.  
  756. isEnabled: function() {
  757. return this._isEnabled;
  758. },
  759.  
  760. getStatus: function() { return this._info.status; },
  761. getMessage: function() { return this._info.message; },
  762. getUrl: function() { return this._info.url; },
  763. getDuration: function() { return parseInt(this._info.duration, 10); },
  764.  
  765. getWidth: function() { return parseInt(this._info.thumbnail.width, 10); },
  766. getHeight: function() { return parseInt(this._info.thumbnail.height, 10); },
  767. getInterval: function() { return parseInt(this._info.thumbnail.interval, 10); },
  768. getCount: function() {
  769. return Math.max(
  770. Math.ceil(this.getDuration() / Math.max(0.01, this.getInterval())),
  771. parseInt(this._info.thumbnail.number, 10)
  772. );
  773. },
  774. getRows: function() { return parseInt(this._info.board.rows, 10); },
  775. getCols: function() { return parseInt(this._info.board.cols, 10); },
  776. getPageCount: function() { return parseInt(this._info.board.number, 10); },
  777. getTotalRows: function() {
  778. return Math.ceil(this.getCount() / this.getCols());
  779. },
  780.  
  781. getPageWidth: function() { return this.getWidth() * this.getCols(); },
  782. getPageHeight: function() { return this.getHeight() * this.getRows(); },
  783. getCountPerPage: function() { return this.getRows() * this.getCols(); },
  784.  
  785. /**
  786. * nページ目のURLを返す。 ゼロオリジン
  787. */
  788. getPageUrl: function(page) {
  789. page = Math.max(0, Math.min(this.getPageCount() - 1, page));
  790. return this.getUrl() + '&board=' + (page + 1);
  791. },
  792.  
  793. /**
  794. * vposに相当するサムネは何番目か?を返す
  795. */
  796. getIndex: function(vpos) {
  797. // msec -> sec
  798. var v = Math.floor(vpos / 1000);
  799. v = Math.max(0, Math.min(this.getDuration(), v));
  800.  
  801. // サムネの総数 ÷ 秒数
  802. // Math.maxはゼロ除算対策
  803. var n = this.getCount() / Math.max(1, this.getDuration());
  804.  
  805. return parseInt(Math.floor(v * n), 10);
  806. },
  807.  
  808. /**
  809. * Indexのサムネイルは何番目のページにあるか?を返す
  810. */
  811. getPageIndex: function(thumbnailIndex) {
  812. var perPage = this.getCountPerPage();
  813. var pageIndex = parseInt(thumbnailIndex / perPage, 10);
  814. return Math.max(0, Math.min(this.getPageCount(), pageIndex));
  815. },
  816.  
  817. /**
  818. * vposに相当するサムネは何ページの何番目にあるか?を返す
  819. */
  820. getThumbnailPosition: function(vpos) {
  821. var thumbnailIndex = this.getIndex(vpos);
  822. var pageIndex = this.getPageIndex(thumbnailIndex);
  823.  
  824. var mod = thumbnailIndex % this.getCountPerPage();
  825. var row = Math.floor(mod / Math.max(1, this.getCols()));
  826. var col = mod % this.getRows();
  827.  
  828. return {
  829. page: pageIndex,
  830. index: thumbnailIndex,
  831. row: row,
  832. col: col
  833. };
  834. },
  835.  
  836. /**
  837. * nページ目のx, y座標をvposに変換して返す
  838. */
  839. getPointVpos: function(x, y, page) {
  840. var width = Math.max(1, this.getWidth());
  841. var height = Math.max(1, this.getHeight());
  842. var row = Math.floor(y / height);
  843. var col = Math.floor(x / width);
  844. var mod = x % width;
  845.  
  846.  
  847. // 何番目のサムネに相当するか?
  848. var point =
  849. page * this.getCountPerPage() +
  850. row * this.getCols() +
  851. col +
  852. (mod / width) // 小数点以下は、n番目の左端から何%あたりか
  853. ;
  854.  
  855. // 全体の何%あたり?
  856. var percent = point / Math.max(1, this.getCount());
  857. percent = Math.max(0, Math.min(100, percent));
  858.  
  859. // vposは㍉秒単位なので1000倍
  860. return Math.floor(this.getDuration() * percent * 1000);
  861. },
  862.  
  863. /**
  864. * vposは何ページ目に当たるか?を返す
  865. */
  866. getVposPage: function(vpos) {
  867. var index = this._storyboard.getIndex(vpos);
  868. var page = this._storyboard.getPageIndex(index);
  869.  
  870. return page;
  871. }
  872.  
  873. });
  874.  
  875. return StoryboardModel;
  876. })();
  877.  
  878. window.NicovideoStoryboard.view.FullScreenModeView = (function() {
  879. var __TEMPLATE__ = (function() {/*
  880. body.full_with_browser{
  881. background: #000;
  882. }
  883. body.full_with_browser.NicovideoStoryboardOpen #content{
  884. margin-bottom: {$storyboardHeight}px;
  885. transition: margin-bottom 0.5s ease-in-out;
  886. }
  887. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  888.  
  889. var addStyle = function(styles, id) {
  890. var elm = document.createElement('style');
  891. window.setTimeout(function() {
  892. elm.type = 'text/css';
  893. if (id) { elm.id = id; }
  894.  
  895. var text = styles.toString();
  896. text = document.createTextNode(text);
  897. elm.appendChild(text);
  898. var head = document.getElementsByTagName('head');
  899. head = head[0];
  900. head.appendChild(elm);
  901. }, 0);
  902. return elm;
  903. };
  904.  
  905. function FullScreenModeView() {
  906.  
  907. this._css = null;
  908. this._lastHeight = -1;
  909. }
  910.  
  911. window.WatchApp.mixin(FullScreenModeView.prototype, {
  912. initialize: function() {
  913. if (this._css) { return; }
  914.  
  915. console.log('%cinitialize NicovideoStorybaordFullScreenStyle', 'background: lightgreen;');
  916. this._css = addStyle('/* undefined */', 'NicovideoStorybaordFullScreenStyle');
  917. },
  918. update: function($container) {
  919. this.initialize();
  920.  
  921. var height = $container.outerHeight();
  922.  
  923. if (height === this._lastHeight) { return; }
  924. this._lastHeight = height;
  925.  
  926. var newCss = __TEMPLATE__.replace('{$storyboardHeight}', height);
  927. this._css.innerHTML = newCss;
  928. }
  929. });
  930.  
  931.  
  932. return FullScreenModeView;
  933. })();
  934.  
  935. window.NicovideoStoryboard.view.SetToEnableButtonView = (function() {
  936.  
  937. var TEXT = {
  938. DEFAULT: 'サムネイルを開く ▲',
  939. LOADING: '動画を読み込み中...',
  940. GETFLV: '動画情報を読み込み中...',
  941. THUMBNAIL: 'サムネイル情報を読み込み中...'
  942. };
  943.  
  944.  
  945. function SetToEnableButtonView(params) {
  946. this._storyboard = params.storyboard;
  947. this._eventDispatcher = params.eventDispatcher;
  948.  
  949. this.initialize();
  950. }
  951.  
  952. window.WatchApp.mixin(SetToEnableButtonView.prototype, {
  953. initialize: function() {
  954. console.log('%c initialize SetToEnableButtonView', 'background: lightgreen;');
  955. this._$view = $([
  956. '<div class="setToEnableButtonContainer loadingVideo">',
  957. '<button>', TEXT.DEFAULT, '</button>',
  958. '</div>',
  959. '',
  960. '',
  961. ''].join(''));
  962. this._$button = this._$view.find('button');
  963. this._$button.on('click', $.proxy(this._onButtonClick, this));
  964.  
  965. var sb = this._storyboard;
  966. sb.addEventListener('reset', $.proxy(this._onStoryboardReset, this));
  967. sb.addEventListener('update', $.proxy(this._onStoryboardUpdate, this));
  968.  
  969. var evt = this._eventDispatcher;
  970. evt.addEventListener('getFlvLoadStart',
  971. $.proxy(this._onGetflvLoadStart, this));
  972. evt.addEventListener('onThumbnailInfoLoadStart',
  973. $.proxy(this._onThumbnailInfoLoadStart, this));
  974. evt.addEventListener('onWatchInfoReset',
  975. $.proxy(this._onWatchInfoReset, this));
  976.  
  977. $('body').append(this._$view);
  978. },
  979. reset: function() {
  980. this._$view.attr('title', '');
  981. if (this._storyboard.isEnabled()) {
  982. this._$view.removeClass('loadingVideo getflv thumbnailInfo fail success').addClass('loading');
  983. this._setText(TEXT.GETFLV);
  984. } else {
  985. this._$view.removeClass('loadingVideo getflv thumbnailInfo fail success loading');
  986. this._setText(TEXT.DEFAULT);
  987. }
  988. },
  989. _setText: function(text) {
  990. this._$button.text(text);
  991. },
  992. _onButtonClick: function(e) {
  993. if (
  994. this._$view.hasClass('loading') ||
  995. this._$view.hasClass('loadingVideo') ||
  996. this._$view.hasClass('fail')) {
  997. return;
  998. }
  999. e.preventDefault();
  1000. e.stopPropagation();
  1001.  
  1002. var $view = this._$view.addClass('loading clicked');
  1003. this._eventDispatcher.dispatchEvent('onEnableStoryboard');
  1004. window.setTimeout(function() {
  1005. $view.removeClass('clicked');
  1006. $view = null;
  1007. }, 1000);
  1008. },
  1009. _onStoryboardReset: function() {
  1010. this.reset();
  1011. },
  1012. _onStoryboardUpdate: function() {
  1013. var storyboard = this._storyboard;
  1014.  
  1015. if (storyboard.getStatus() === 'ok') {
  1016. window.setTimeout($.proxy(function() {
  1017. this._$view
  1018. .removeClass('loading getflv thumbnailInfo')
  1019. .addClass('success')
  1020. .attr('title', '');
  1021. this._setText(TEXT.DEFAULT);
  1022. }, this), 3000);
  1023. } else {
  1024. this._$view
  1025. .removeClass('loading')
  1026. .addClass('fail')
  1027. .attr('title', DEBUG ? noThumbnailAA : '');
  1028. this._setText(storyboard.getMessage());
  1029. }
  1030. },
  1031. _onGetflvLoadStart: function() {
  1032. this._$view.addClass('loading getflv');
  1033. this._setText(TEXT.GETFLV);
  1034. },
  1035. _onThumbnailInfoLoadStart: function() {
  1036. this._$view.addClass('loading thumbnailInfo');
  1037. this._setText(TEXT.THUMBNAIL);
  1038. },
  1039. _onWatchInfoReset: function() {
  1040. this._$view.addClass('loadingVideo');
  1041. this._setText(TEXT.LOADING);
  1042. }
  1043. });
  1044.  
  1045. return SetToEnableButtonView;
  1046. })();
  1047.  
  1048. window.NicovideoStoryboard.view.StoryboardView = (function() {
  1049. var TIMER_INTERVAL = 33;
  1050. var VPOS_RATE = 10;
  1051.  
  1052. function StoryboardView(params) {
  1053. this.initialize(params);
  1054. }
  1055.  
  1056. window.WatchApp.mixin(StoryboardView.prototype, {
  1057. initialize: function(params) {
  1058. console.log('%c initialize StoryboardView', 'background: lightgreen;');
  1059.  
  1060. this._watchController = params.watchController;
  1061. var evt = this._eventDispatcher = params.eventDispatcher;
  1062. var sb = this._storyboard = params.storyboard;
  1063.  
  1064. this._isHover = false;
  1065. this._currentUrl = '';
  1066. this._lazyImage = {};
  1067. this._lastPage = -1;
  1068. this._lastVpos = 0;
  1069. this._lastGetVpos = 0;
  1070. this._timerCount = 0;
  1071. this._scrollLeft = 0;
  1072.  
  1073. this._enableButtonView =
  1074. new window.NicovideoStoryboard.view.SetToEnableButtonView({
  1075. storyboard: sb,
  1076. eventDispatcher: this._eventDispatcher
  1077. });
  1078.  
  1079. this._fullScreenModeView =
  1080. new window.NicovideoStoryboard.view.FullScreenModeView();
  1081.  
  1082. evt.addEventListener('onWatchInfoReset', $.proxy(this._onWatchInfoReset, this));
  1083.  
  1084. sb.addEventListener('update', $.proxy(this._onStoryboardUpdate, this));
  1085. sb.addEventListener('reset', $.proxy(this._onStoryboardReset, this));
  1086. sb.addEventListener('unload', $.proxy(this._onStoryboardUnload, this));
  1087. },
  1088. _initializeStoryboard: function() {
  1089. this._initializeStoryboard = _.noop;
  1090. console.log('%cStoryboardView.initializeStoryboard', 'background: lightgreen;');
  1091.  
  1092. var $view = this._$view = $(storyboardTemplate);
  1093.  
  1094. var $inner = this._$inner = $view.find('.storyboardInner');
  1095. this._$failMessage = $view.find('.failMessage');
  1096. this._$cursorTime = $view.find('.cursorTime');
  1097. this._$disableButton = $view.find('.setToDisable button');
  1098.  
  1099. $view
  1100. .on('click', '.board',
  1101. $.proxy(this._onBoardClick, this))
  1102. .on('mousemove', '.board',
  1103. $.proxy(this._onBoardMouseMove, this))
  1104. .on('mousemove', '.board',
  1105. _.debounce($.proxy(this._onBoardMouseMoveEnd, this), 300))
  1106. .on('mousewheel',
  1107. $.proxy(this._onMouseWheel, this))
  1108. .on('mousewheel',
  1109. _.debounce($.proxy(this._onMouseWheelEnd, this), 300));
  1110.  
  1111. var self = this;
  1112. var onHoverIn = function() { self._isHover = true; };
  1113. var onHoverOut = function() { self._isHover = false; };
  1114. $inner
  1115. .hover(onHoverIn, onHoverOut)
  1116. .on('touchstart', $.proxy(this._onTouchStart, this))
  1117. .on('touchend', $.proxy(this._onTouchEnd, this))
  1118. // .on('touchcancel', $.proxy(this._onTouchCancel, this))
  1119. .on('touchmove', $.proxy(this._onTouchMove, this))
  1120. .on('scroll', _.throttle(function() { self._onScroll(); }, 500));
  1121.  
  1122. this._watchController
  1123. .addEventListener('onVideoSeeked', $.proxy(this._onVideoSeeked, this));
  1124.  
  1125. this._watchController
  1126. .addEventListener('onVideoSeeking', $.proxy(this._onVideoSeeking, this));
  1127.  
  1128. this._$disableButton.on('click',
  1129. $.proxy(this._onDisableButtonClick, this));
  1130.  
  1131. $('body')
  1132. .append($view)
  1133. .on('touchend', function() { self._isHover = false; });
  1134. },
  1135. _onBoardClick: function(e) {
  1136. var $board = $(e.target), offset = $board.offset();
  1137. var y = $board.attr('data-top') * 1;
  1138. var x = e.pageX - offset.left;
  1139. var page = $board.attr('data-page');
  1140. var vpos = this._storyboard.getPointVpos(x, y, page);
  1141. if (isNaN(vpos)) { return; }
  1142.  
  1143. var $view = this._$view;
  1144. $view.addClass('clicked');
  1145. window.setTimeout(function() { $view.removeClass('clicked'); }, 1000);
  1146. this._eventDispatcher.dispatchEvent('onStoryboardSelect', vpos);
  1147. this._$cursorTime.css({left: -999});
  1148.  
  1149. this._isHover = false;
  1150. if ($board.hasClass('lazyImage')) { this._lazyLoadImage(page); }
  1151. },
  1152. _onBoardMouseMove: function(e) {
  1153. var $board = $(e.target), offset = $board.offset();
  1154. var y = $board.attr('data-top') * 1;
  1155. var x = e.pageX - offset.left;
  1156. var page = $board.attr('data-page');
  1157. var vpos = this._storyboard.getPointVpos(x, y, page);
  1158. if (isNaN(vpos)) { return; }
  1159. var sec = Math.floor(vpos / 1000);
  1160.  
  1161. var time = Math.floor(sec / 60) + ':' + ((sec % 60) + 100).toString().substr(1);
  1162. this._$cursorTime.text(time).css({left: e.pageX});
  1163.  
  1164. this._isHover = true;
  1165. this._isMouseMoving = true;
  1166. if ($board.hasClass('lazyImage')) { this._lazyLoadImage(page); }
  1167. },
  1168. _onBoardMouseMoveEnd: function(e) {
  1169. this._isMouseMoving = false;
  1170. },
  1171. _onMouseWheel: function(e, delta) {
  1172. e.preventDefault();
  1173. e.stopPropagation();
  1174. this._isHover = true;
  1175. this._isMouseMoving = true;
  1176. var left = this.scrollLeft();
  1177. this.scrollLeft(left - delta * 140);
  1178. },
  1179. _onMouseWheelEnd: function(e, delta) {
  1180. this._isMouseMoving = false;
  1181. },
  1182. _onTouchStart: function(e) {
  1183. e.stopPropagation();
  1184. },
  1185. _onTouchEnd: function(e) {
  1186. e.stopPropagation();
  1187. // window.setTimeout($.proxy(function() { this._isHover = false; }, this), 100);
  1188. },
  1189. _onTouchMove: function(e) {
  1190. e.stopPropagation();
  1191. this._isHover = true;
  1192. },
  1193. _onTouchCancel: function(e) {
  1194. },
  1195. _onVideoSeeking: function() {
  1196. },
  1197. _onVideoSeeked: function() {
  1198. if (!this._storyboard.isEnabled()) {
  1199. return;
  1200. }
  1201. if (this._storyboard.getStatus() !== 'ok') {
  1202. return;
  1203. }
  1204. var vpos = this._watchController.getVpos();
  1205. var page = this._storyboard.getVposPage(vpos);
  1206.  
  1207. this._lazyLoadImage(page);
  1208. if (this.isHover || !this._watchController.isPlaying()) {
  1209. this._onVposUpdate(vpos, true);
  1210. } else {
  1211. this._onVposUpdate(vpos);
  1212. }
  1213. },
  1214. update: function() {
  1215. this.disableTimer();
  1216.  
  1217. this._initializeStoryboard();
  1218. this._$view.removeClass('show success');
  1219. $('body').removeClass('NicovideoStoryboardOpen');
  1220. if (this._storyboard.getStatus() === 'ok') {
  1221. this._updateSuccess();
  1222. } else {
  1223. this._updateFail();
  1224. }
  1225. },
  1226. scrollLeft: function(left) {
  1227. if (left === undefined) {
  1228. return this._scrollLeft;
  1229. } else
  1230. if (left === 0 || Math.abs(this._scrollLeft - left) >= 1) {
  1231. this._$inner[0].scrollLeft = left;
  1232. this._scrollLeft = left;
  1233. }
  1234. },
  1235. _updateSuccess: function() {
  1236. var url = this._storyboard.getUrl();
  1237. var $view = this._$view.addClass('opening');
  1238.  
  1239. if (this._currentUrl === url) {
  1240. $view.addClass('show success');
  1241. this.enableTimer();
  1242. } else {
  1243. this._currentUrl = url;
  1244. this._updateSuccessFull();
  1245. }
  1246. $('body').addClass('NicovideoStoryboardOpen');
  1247.  
  1248. window.setTimeout(function() {
  1249. $view.removeClass('opening');
  1250. $view = null;
  1251. }, 1000);
  1252. },
  1253. _updateSuccessFull: function() {
  1254. var storyboard = this._storyboard;
  1255. var pages = storyboard.getPageCount();
  1256. var pageWidth = storyboard.getPageWidth();
  1257. var height = storyboard.getHeight();
  1258. var rows = storyboard.getRows();
  1259.  
  1260. var $borders =
  1261. this._createBorders(storyboard.getWidth(), storyboard.getHeight(), storyboard.getCols());
  1262.  
  1263. var totalRows = storyboard.getTotalRows();
  1264. var rowCnt = 0;
  1265. var $list = $('<div class="boardList"/>')
  1266. .css({
  1267. width: storyboard.getCount() * storyboard.getWidth(),
  1268. paddingLeft: '50%',
  1269. paddingRight: '50%',
  1270. height: height
  1271. });
  1272.  
  1273. for (var i = 0; i < pages; i++) {
  1274. var src = storyboard.getPageUrl(i);
  1275. for (var j = 0; j < rows; j++) {
  1276. var $img =
  1277. $('<div class="board"/>')
  1278. .css({
  1279. width: pageWidth,
  1280. height: height,
  1281. backgroundPosition: '0 -' + height * j + 'px'
  1282. })
  1283. .attr({
  1284. 'data-src': src,
  1285. 'data-page': i,
  1286. 'data-top': height * j + height / 2
  1287. })
  1288. .append($borders.clone());
  1289.  
  1290. if (i === 0) { // 1ページ目だけ遅延ロードしない
  1291. $img.css('background-image', 'url(' + src + ')');
  1292. } else {
  1293. $img.addClass('lazyImage page-' + i);
  1294. }
  1295. $list.append($img);
  1296. rowCnt++;
  1297. if (rowCnt >= totalRows) {
  1298. break;
  1299. }
  1300. }
  1301. }
  1302.  
  1303. this._$innerList = $list;
  1304.  
  1305. this._$inner.empty().append($list).append(this._$pointer);
  1306. this._$view.removeClass('fail').addClass('success');
  1307.  
  1308. this._fullScreenModeView.update(this._$view);
  1309.  
  1310. window.setTimeout($.proxy(function() {
  1311. this._$view.addClass('show');
  1312. }, this), 100);
  1313.  
  1314. this.scrollLeft(0);
  1315. this.enableTimer();
  1316. },
  1317. _createBorders: function(width, height, count) {
  1318. var $border = $('<div class="border"/>').css({
  1319. width: width,
  1320. height: height
  1321. });
  1322. var $div = $('<div />');
  1323. for (var i = 0; i < count; i++) {
  1324. $div.append($border.clone());
  1325. }
  1326. return $div;
  1327. },
  1328. _lazyLoadImage: function(pageNumber) {
  1329. var className = 'page-' + pageNumber;
  1330.  
  1331. if (pageNumber < 1 || this._lazyImage[className]) {
  1332. return;
  1333. }
  1334.  
  1335. var src = this._storyboard.getPageUrl(pageNumber);
  1336. this._lazyImage[className] = src;
  1337.  
  1338. //console.log('%c set lazyLoadImage', 'background: cyan;', 'page: ' + pageNumber, ' url: ' + src);
  1339.  
  1340. var load = $.proxy(function() {
  1341. this._$inner.find('.' + className)
  1342. .css('background-image', 'url(' + src + ')')
  1343. .removeClass('lazyImage ' + className);
  1344. }, this);
  1345.  
  1346. window.setTimeout(load, 0);
  1347. //window.setTimeout(load, 1000);
  1348. },
  1349. _updateFail: function() {
  1350. this._$view.removeClass('success').addClass('fail');
  1351. this.disableTimer();
  1352. },
  1353. clear: function() {
  1354. if (this._$view) {
  1355. this._$inner.empty();
  1356. }
  1357. this.disableTimer();
  1358. },
  1359. _clearTimer: function() {
  1360. if (this._timer) {
  1361. window.clearInterval(this._timer);
  1362. this._timer = null;
  1363. }
  1364. },
  1365. enableTimer: function() {
  1366. this._clearTimer();
  1367. this._isHover = false;
  1368. this._timer = window.setInterval($.proxy(this._onTimerInterval, this), TIMER_INTERVAL);
  1369. },
  1370. disableTimer: function() {
  1371. this._clearTimer();
  1372. },
  1373. _onTimerInterval: function() {
  1374. if (this._isHover) { return; }
  1375. if (!this._storyboard.isEnabled()) { return; }
  1376.  
  1377. var div = VPOS_RATE;
  1378. var mod = this._timerCount % div;
  1379. this._timerCount++;
  1380.  
  1381. var vpos;
  1382.  
  1383. if (!this._watchController.isPlaying()) {
  1384. return;
  1385. }
  1386.  
  1387. // getVposが意外に時間を取るので回数を減らす
  1388. // そもそもコメントパネルがgetVpos叩きまくってるんですがそれは
  1389. if (mod === 0) {
  1390. vpos = this._watchController.getVpos();
  1391. } else {
  1392. vpos = this._lastVpos;
  1393. }
  1394.  
  1395. this._onVposUpdate(vpos);
  1396. },
  1397. _onVposUpdate: function(vpos, isImmediately) {
  1398. var storyboard = this._storyboard;
  1399. var duration = Math.max(1, storyboard.getDuration());
  1400. var per = vpos / (duration * 1000);
  1401. var width = storyboard.getWidth();
  1402. var boardWidth = storyboard.getCount() * width;
  1403. var targetLeft = boardWidth * per + width * 0.4;
  1404. var currentLeft = this.scrollLeft();
  1405. var leftDiff = targetLeft - currentLeft;
  1406.  
  1407. if (Math.abs(leftDiff) > 5000) {
  1408. leftDiff = leftDiff * 0.93; // 大きくシークした時
  1409. } else {
  1410. leftDiff = leftDiff / VPOS_RATE;
  1411. }
  1412.  
  1413. this._lastVpos = vpos;
  1414.  
  1415. this.scrollLeft(isImmediately ? targetLeft : (currentLeft + Math.round(leftDiff)));
  1416.  
  1417. },
  1418. _onScroll: function() {
  1419. var storyboard = this._storyboard;
  1420. var scrollLeft = this.scrollLeft();
  1421. var page = Math.round(scrollLeft / (storyboard.getPageWidth() * storyboard.getRows()));
  1422. this._lazyLoadImage(Math.min(page, storyboard.getPageCount() - 1));
  1423. },
  1424. reset: function() {
  1425. this._lastVpos = -1;
  1426. this._lastPage = -1;
  1427. this._currentUrl = '';
  1428. this._timerCount = 0;
  1429. this._scrollLeft = 0;
  1430. this._lazyImage = {};
  1431. if (this._$view) {
  1432. $('body').removeClass('NicovideoStoryboardOpen');
  1433. this._$view.removeClass('show');
  1434. this._$inner.empty();
  1435. }
  1436. },
  1437. _onDisableButtonClick: function(e) {
  1438. e.preventDefault();
  1439. e.stopPropagation();
  1440.  
  1441. var $button = this._$disableButton;
  1442. $button.addClass('clicked');
  1443. window.setTimeout(function() {
  1444. $button.removeClass('clicked');
  1445. }, 1000);
  1446.  
  1447. this._eventDispatcher.dispatchEvent('onDisableStoryboard');
  1448. },
  1449. _onStoryboardUpdate: function() {
  1450. this.update();
  1451. },
  1452. _onStoryboardReset: function() {
  1453. },
  1454. _onStoryboardUnload: function() {
  1455. $('body').removeClass('NicovideoStoryboardOpen');
  1456. if (this._$view) {
  1457. this._$view.removeClass('show');
  1458. }
  1459. },
  1460. _onWatchInfoReset: function() {
  1461. this.reset();
  1462. }
  1463. });
  1464.  
  1465. return StoryboardView;
  1466. })();
  1467.  
  1468. window.NicovideoStoryboard.controller.StoryboardController = (function() {
  1469.  
  1470. function StoryboardController(params) {
  1471. this.initialize(params);
  1472. }
  1473.  
  1474. window.WatchApp.mixin(StoryboardController.prototype, {
  1475. initialize: function(params) {
  1476. console.log('%c initialize StoryboardController', 'background: lightgreen;');
  1477.  
  1478. this._thumbnailInfo = params.thumbnailInfo;
  1479. this._watchController = params.watchController;
  1480. this._config = params.config;
  1481.  
  1482. var evt = this._eventDispatcher = params.eventDispatcher;
  1483.  
  1484. evt.addEventListener('onVideoInitialized',
  1485. $.proxy(this._onVideoInitialized, this));
  1486.  
  1487. evt.addEventListener('onWatchInfoReset',
  1488. $.proxy(this._onWatchInfoReset, this));
  1489.  
  1490. evt.addEventListener('onStoryboardSelect',
  1491. $.proxy(this._onStoryboardSelect, this));
  1492.  
  1493. evt.addEventListener('onEnableStoryboard',
  1494. $.proxy(this._onEnableStoryboard, this));
  1495.  
  1496. evt.addEventListener('onDisableStoryboard',
  1497. $.proxy(this._onDisableStoryboard, this));
  1498.  
  1499. evt.addEventListener('onGetflvLoadStart',
  1500. $.proxy(this._onGetflvLoadStart, this));
  1501.  
  1502. evt.addEventListener('onThumbnailInfoLoadStart',
  1503. $.proxy(this._onThumbnailInfoLoadStart, this));
  1504.  
  1505. evt.addEventListener('onThumbnailInfoLoad',
  1506. $.proxy(this._onThumbnailInfoLoad, this));
  1507.  
  1508. this._initializeStoryboard();
  1509. },
  1510.  
  1511. _initializeStoryboard: function() {
  1512. this._initializeStoryboard = _.noop;
  1513.  
  1514. if (!this._storyboardModel) {
  1515. var nsv = window.NicovideoStoryboard;
  1516. this._storyboardModel = new nsv.model.StoryboardModel({
  1517. thumbnailInfo: this._thumbnailInfo,
  1518. isEnabled: this._config.get('enabled') === true,
  1519. watchId: this._watchController.getWatchId()
  1520. });
  1521. }
  1522. if (!this._storyboardView) {
  1523. this._storyboardView = new window.NicovideoStoryboard.view.StoryboardView({
  1524. watchController: this._watchController,
  1525. eventDispatcher: this._eventDispatcher,
  1526. storyboard: this._storyboardModel
  1527. });
  1528. }
  1529. },
  1530.  
  1531. load: function(watchId) {
  1532. if (watchId) {
  1533. this._storyboardModel.setWatchId(watchId);
  1534. }
  1535. this._storyboardModel.load();
  1536. },
  1537.  
  1538. unload: function() {
  1539. if (this._storyboardModel) {
  1540. this._storyboardModel.unload();
  1541. }
  1542. },
  1543.  
  1544. _onVideoInitialized: function() {
  1545. this._initializeStoryboard();
  1546. this._storyboardModel.reset();
  1547. },
  1548.  
  1549. _onWatchInfoReset: function() {
  1550. this._storyboardModel.setWatchId(this._watchController.getWatchId());
  1551. },
  1552.  
  1553. _onThumbnailInfoLoad: function(info) {
  1554. //console.log('StoryboardController._onThumbnailInfoLoad', info);
  1555.  
  1556. this._storyboardModel.update(info);
  1557. },
  1558.  
  1559. _onStoryboardSelect: function(vpos) {
  1560. //console.log('_onStoryboardSelect', vpos);
  1561. this._watchController.setVpos(vpos);
  1562. },
  1563.  
  1564. _onEnableStoryboard: function() {
  1565. window.setTimeout($.proxy(function() {
  1566. this._config.set('enabled', true);
  1567. this.load();
  1568. }, this), 0);
  1569. },
  1570.  
  1571. _onDisableStoryboard: function() {
  1572. window.setTimeout($.proxy(function() {
  1573. this._config.set('enabled', false);
  1574. this.unload();
  1575. }, this), 0);
  1576. },
  1577.  
  1578. _onGetflvLoadStart: function() {
  1579. },
  1580.  
  1581. _onThumbnailInfoLoadStart: function() {
  1582. }
  1583. });
  1584.  
  1585. return StoryboardController;
  1586. })();
  1587.  
  1588.  
  1589. window.WatchApp.mixin(window.NicovideoStoryboard, {
  1590. _addStyle: function(styles, id) {
  1591. var elm = document.createElement('style');
  1592. window.setTimeout(function() {
  1593. elm.type = 'text/css';
  1594. if (id) { elm.id = id; }
  1595.  
  1596. var text = styles.toString();
  1597. text = document.createTextNode(text);
  1598. elm.appendChild(text);
  1599. var head = document.getElementsByTagName('head');
  1600. head = head[0];
  1601. head.appendChild(elm);
  1602. }, 0);
  1603. return elm;
  1604. },
  1605. initialize: function() {
  1606. console.log('%c initialize NicovideoStoryboard', 'background: lightgreen;');
  1607. this._initializeUserConfig();
  1608.  
  1609. this._getflv = window.NicovideoStoryboard.api.getflv;
  1610. this._thumbnailInfo = window.NicovideoStoryboard.api.thumbnailInfo;
  1611. this._watchController = window.NicovideoStoryboard.external.watchController;
  1612.  
  1613. this._eventDispatcher = new EventDispatcher();
  1614.  
  1615. if (!this._watchController.isPremium()) {
  1616. this._watchController.popup.alert('NicovideoStoryboardはプレミアムの機能を使っているため、一般アカウントでは動きません');
  1617. return;
  1618. }
  1619.  
  1620. this._storyboardController = new window.NicovideoStoryboard.controller.StoryboardController({
  1621. thumbnailInfo: this._thumbnailInfo,
  1622. watchController: this._watchController,
  1623. eventDispatcher: this._eventDispatcher,
  1624. config: this.config
  1625. });
  1626.  
  1627. this._initializeEvent();
  1628. this._initializeSettingPanel();
  1629.  
  1630. this._addStyle(__css__, 'NicovideoStoryboardCss');
  1631. },
  1632. _initializeEvent: function() {
  1633. console.log('%c initializeEvent NicovideoStoryboard', 'background: lightgreen;');
  1634.  
  1635. var eventDispatcher = this._eventDispatcher;
  1636.  
  1637. this._watchController.addEventListener('onWatchInfoReset', function() {
  1638. eventDispatcher.dispatchEvent('onWatchInfoReset');
  1639. });
  1640.  
  1641. this._watchController.addEventListener('onVideoInitialized', function() {
  1642. eventDispatcher.dispatchEvent('onVideoInitialized');
  1643. });
  1644.  
  1645. this._getflv.addEventListener('onGetflvLoadStart', function() {
  1646. eventDispatcher.dispatchEvent('onGetflvLoadStart');
  1647. });
  1648. this._getflv.addEventListener('onGetflvLoad', function(info) {
  1649. eventDispatcher.dispatchEvent('onGetflvLoad', info);
  1650. });
  1651.  
  1652. this._thumbnailInfo.addEventListener('onThumbnailInfoLoadStart', function() {
  1653. eventDispatcher.dispatchEvent('onThumbnailInfoLoadStart');
  1654. });
  1655. this._thumbnailInfo.addEventListener('onThumbnailInfoLoad', $.proxy(function(info) {
  1656. eventDispatcher.dispatchEvent('onThumbnailInfoLoad', info);
  1657. this._onThumbnailInfoLoad(info);
  1658. }, this));
  1659.  
  1660. },
  1661. _initializeUserConfig: function() {
  1662. var prefix = 'NicoStoryboard_';
  1663. var conf = {
  1664. enabled: true,
  1665. autoScroll: true,
  1666. demoMode: false
  1667. };
  1668.  
  1669. this.config = {
  1670. get: function(key) {
  1671. try {
  1672. if (window.localStorage.hasOwnProperty(prefix + key)) {
  1673. return JSON.parse(window.localStorage.getItem(prefix + key));
  1674. }
  1675. return conf[key];
  1676. } catch (e) {
  1677. return conf[key];
  1678. }
  1679. },
  1680. set: function(key, value) {
  1681. window.localStorage.setItem(prefix + key, JSON.stringify(value));
  1682. }
  1683. };
  1684. },
  1685. load: function(watchId) {
  1686. // 動画ごとのcookieがないと取得できないので指定できてもあまり意味は無い
  1687. watchId = watchId || this._watchController.getWatchId();
  1688. this._storyboardController.load(watchId);
  1689. },
  1690. _initializeSettingPanel: function() {
  1691. var $menu = $('<li class="NicoVideoStoryboardSettingMenu"><a href="javascript:;" title="NicoVideoStoryboardの設定変更">NicoVideo-<br>Storyboard設定</a></li>');
  1692. var $panel = $('<div id="NicoVideoStoryboardSettingPanel" />');//.addClass('open');
  1693. //var $button = $('<button class="toggleSetting playerBottomButton">設定</botton>');
  1694.  
  1695. //$button.on('click', function(e) {
  1696. // e.stopPropagation(); e.preventDefault();
  1697. // $panel.toggleClass('open');
  1698. //});
  1699.  
  1700. var config = this.config, eventDispatcher = this._eventDispatcher;
  1701. $menu.find('a').on('click', function() { $panel.toggleClass('open'); });
  1702.  
  1703. var __tpl__ = (function() {/*
  1704. <div class="panelHeader">
  1705. <h1 class="windowTitle">NicoVideoStoryboardの設定</h1>
  1706. <button class="close" title="閉じる">×</button>
  1707. </div>
  1708. <div class="panelInner">
  1709. <div class="item" data-setting-name="demoMode" data-menu-type="radio">
  1710. <h3 class="itemTitle">デモモード</h3>
  1711. <p>連続再生時、サムネイルが無い動画をスキップします</p>
  1712. <label><input type="radio" value="true" > ON</label>
  1713. <label><input type="radio" value="false"> OFF</label>
  1714. </div>
  1715. </div>
  1716. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  1717. $panel.html(__tpl__);
  1718. $panel.find('.item').on('click', function(e) {
  1719. var $this = $(this);
  1720. var settingName = $this.attr('data-setting-name');
  1721. var value = JSON.parse($this.find('input:checked').val());
  1722. var currentValue = config.get(settingName);
  1723. if (currentValue !== value) {
  1724. console.log('%cseting-name: ' + settingName, 'background: cyan', 'value', value);
  1725. config.set(settingName, value);
  1726. eventDispatcher.dispatchEvent('NicoVideoStoryboard.config.' + settingName, value);
  1727. }
  1728. }).each(function(e) {
  1729. var $this = $(this);
  1730. var settingName = $this.attr('data-setting-name');
  1731. var value = config.get(settingName);
  1732. $this.addClass(settingName);
  1733. $this.find('input').attr('name', settingName).val([JSON.stringify(value)]);
  1734. });
  1735. $panel.find('.close').click(function() {
  1736. $panel.removeClass('open');
  1737. });
  1738.  
  1739.  
  1740. $('#siteHeaderRightMenuFix').after($menu);
  1741. $('body').append($panel);
  1742. },
  1743. _onThumbnailInfoLoad: function(info) {
  1744. if (
  1745. info.status !== 'ok' &&
  1746. this.config.get('demoMode') === true &&
  1747. this._watchController.playlist.isContinuous()
  1748. ) {
  1749. this._watchController.playlist.playNext();
  1750. }
  1751. }
  1752.  
  1753. });
  1754.  
  1755.  
  1756. //======================================
  1757. //======================================
  1758. //======================================
  1759.  
  1760. (function() {
  1761. var watchInfoModel = window.WatchApp.ns.model.WatchInfoModel.getInstance();
  1762. if (watchInfoModel.initialized) {
  1763. console.log('%c initialize', 'background: lightgreen;');
  1764. window.NicovideoStoryboard.initialize();
  1765. } else {
  1766. var onReset = function() {
  1767. watchInfoModel.removeEventListener('reset', onReset);
  1768. window.setTimeout(function() {
  1769. watchInfoModel.removeEventListener('reset', onReset);
  1770. console.log('%c initialize', 'background: lightgreen;');
  1771. window.NicovideoStoryboard.initialize();
  1772. }, 0);
  1773. };
  1774. watchInfoModel.addEventListener('reset', onReset);
  1775. }
  1776. })();
  1777.  
  1778. };
  1779.  
  1780. var flapi = function() {
  1781. if (window.name.indexOf('getflvLoader') < 0 ) { return; }
  1782.  
  1783. var resp = document.documentElement.textContent;
  1784. var origin = 'http://' + location.host.replace(/^.*?\./, 'www.');
  1785.  
  1786. try {
  1787. parent.postMessage(JSON.stringify({
  1788. id: 'NicovideoStoryboard',
  1789. type: 'getflv',
  1790. body: {
  1791. url: location.href,
  1792. info: resp
  1793. }
  1794. }),
  1795. origin);
  1796. } catch (e) {
  1797. alert(e);
  1798. console.log('err', e);
  1799. }
  1800. };
  1801.  
  1802.  
  1803. var smileapi = function() {
  1804. if (window.name.indexOf('StoryboardLoader') < 0 ) { return; }
  1805.  
  1806. var resp = document.getElementsByTagName('smile');
  1807. var origin = 'http://' + location.host.replace(/^.*?\./, 'www.');
  1808. var xml = '';
  1809.  
  1810. if (resp.length > 0) {
  1811. xml = resp[0].outerHTML;
  1812. }
  1813.  
  1814. try {
  1815. parent.postMessage(JSON.stringify({
  1816. id: 'NicovideoStoryboard',
  1817. type: 'storyboard',
  1818. body: {
  1819. url: location.href,
  1820. xml: xml
  1821. }
  1822. }),
  1823. origin);
  1824. } catch (e) {
  1825. console.log('err', e);
  1826. }
  1827. };
  1828.  
  1829.  
  1830.  
  1831. var host = window.location.host || '';
  1832. if (host === 'flapi.nicovideo.jp') {
  1833. flapi();
  1834. } else
  1835. if (host.indexOf('smile-') >= 0) {
  1836. smileapi();
  1837. } else {
  1838. var script = document.createElement('script');
  1839. script.id = 'NicoVideoStoryboard';
  1840. script.setAttribute('type', 'text/javascript');
  1841. script.setAttribute('charset', 'UTF-8');
  1842. script.appendChild(document.createTextNode("(" + monkey + ")()"));
  1843. document.body.appendChild(script);
  1844. }
  1845.  
  1846. })();