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