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