WatchWatch

GINZAwatchに時計復活。 ついでに過去ログ時間選択も使いやすくする

  1. // ==UserScript==
  2. // @name WatchWatch
  3. // @namespace https://github.com/segabito/
  4. // @description GINZAwatchに時計復活。 ついでに過去ログ時間選択も使いやすくする
  5. // @include http://www.nicovideo.jp/watch/*
  6. // @version 0.1.6
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10. (function() {
  11. var monkey = (function() {
  12. 'use strict';
  13.  
  14. if (!window.WatchJsApi) {
  15. return;
  16. }
  17.  
  18. window.WatchWatch = {};
  19.  
  20. var console = {
  21. log: function() {},
  22. trace: function() {}
  23. };
  24.  
  25. if (localStorage.watchItLater_debugMode === 'true') {
  26. console = window.console;
  27. }
  28.  
  29. var _ = require('lodash');
  30. var inherit = require('cjs!inherit');
  31. var advice = require('advice');
  32. var EventDispatcher = require('watchapp/event/EventDispatcher');
  33.  
  34. var TimeMachine = (function() {
  35. function TimeMachine(playerInitializer) {
  36. EventDispatcher.call(this);
  37. this._initialize(playerInitializer);
  38. }
  39. inherit(TimeMachine, EventDispatcher);
  40.  
  41. _.assign(TimeMachine.prototype, {
  42. _initialize: function(playerInitializer) {
  43. var commentPanelView = this._commentPanelView =
  44. playerInitializer.rightSidePanelViewController._playerPanelTabsView._commentPanelView;
  45. advice.after(commentPanelView, '_showLogForm', _.bind(function(date) {
  46. this.dispatchEvent('timeMachine-changePastMode', true, date);
  47. }, this));
  48. advice.after(commentPanelView, '_hideLogForm', _.bind(function() {
  49. this.dispatchEvent('timeMachine-changePastMode', false);
  50. }, this));
  51. },
  52. goToPast: function(tm) {
  53. this._commentPanelView.loadPastComments(tm);
  54. },
  55. goToPresent: function() {
  56. this._commentPanelView.loadPastComments();
  57. }
  58. });
  59.  
  60. return TimeMachine;
  61. })();
  62.  
  63.  
  64. _.assign(window.WatchWatch, {
  65. _isPastMode: false,
  66. _pastTime: (new Date()).getTime(),
  67. getPastTime: function() {
  68. if (this._isPastMode) {
  69. return this._pastTime;
  70. } else {
  71. return (new Date()).getTime();
  72. }
  73. },
  74. initialize: function() {
  75. var PlayerInitializer = require('watchapp/init/PlayerInitializer');
  76. this._watchInfoModel = require('watchapp/model/WatchInfoModel').getInstance();
  77. this._playerAreaConnector = PlayerInitializer.playerAreaConnector;
  78. this._nicoPlayerConnector = PlayerInitializer.nicoPlayerConnector;
  79. this._timeMachine = new TimeMachine(PlayerInitializer);
  80.  
  81. var EventDispatcher = require('watchapp/event/EventDispatcher');
  82. this.event = new EventDispatcher();
  83. this.initializeCss();
  84.  
  85. this.initializeUserConfig();
  86.  
  87. this.initializeDom();
  88. this.initializeTimeMachine();
  89. this.initializeTimer();
  90.  
  91. },
  92. addStyle: function(styles, id) {
  93. var elm = document.createElement('style');
  94. elm.type = 'text/css';
  95. if (id) { elm.id = id; }
  96.  
  97. var text = styles.toString();
  98. text = document.createTextNode(text);
  99. elm.appendChild(text);
  100. var head = document.getElementsByTagName('head');
  101. head = head[0];
  102. head.appendChild(elm);
  103. return elm;
  104. },
  105. initializeCss: function() {
  106. var __css__ = (function() {/*
  107. .watchWatch #playerCommentPanel .playerCommentPanelHeader .currentThreadName,
  108. .watchWatch .select-box.comment-type
  109. {
  110. width: 120px;
  111. }
  112.  
  113. .watchWatch #playerCommentPanel .playerCommentPanelHeader .comment,
  114. .watchWatch .header-icon.comment-log
  115. {
  116. display: none;
  117. }
  118.  
  119. .watchWatch #playerCommentPanel #commentLogHeader form,
  120. .watchWatch .comment-panel .log-form
  121. {
  122. display: none;
  123. }
  124.  
  125. .watchWatch #playerCommentPanel .section #commentLog.commentTable,
  126. .watchWatch .comment-panel.log-form-opened .panel-body
  127. {
  128. top: 28px;
  129. }
  130.  
  131. .watchWatch .commentPanelAlert,
  132. .watchWatch .comment-panel .panel-alert
  133. {
  134. top: 28px;
  135. }
  136.  
  137.  
  138. .watchWatchContainer {
  139. position: absolute;
  140. left: 139px;
  141. top: 5px;
  142. -moz-box-sizing: border-box;
  143. -webkit-box-sizing: border-box;
  144. width: 140px;
  145. font-size: 10px;
  146. padding: 5px 4px 2px;
  147. text-align: center;
  148. white-space: nowrap;
  149. overflow: hidden;
  150. background: #f4f4f4;
  151. border: 1px solid #999;
  152. cursor: pointer;
  153. z-index: 10060;
  154. }
  155. .watchWatchContainer:hover {
  156. background: #fff;
  157. color: #008;
  158. }
  159. .watchWatchContainer.past {
  160. color: red;
  161. }
  162.  
  163.  
  164.  
  165.  
  166. .timeMachineControl.show {
  167. display: block;
  168. }
  169. .timeMachineControl {
  170. position: absolute;
  171. display: none;
  172. top: 34px;
  173. left: 6px;
  174. z-index: 10060;
  175. -moz-box-sizing: border-box;
  176. -webkit-box-sizing: border-box;
  177. width: 313px;
  178. overflow: hidden;
  179. background: #fff;
  180. box-shadow: 0 0 4px black;
  181. }
  182.  
  183. .timeMachineControl .reset,
  184. .timeMachineControl .title {
  185. margin: 8px;
  186. }
  187. .timeMachineControl .datepickerContainer {
  188. }
  189. .timeMachineControl .datepickerContainer .ui-datepicker {
  190. display: block;
  191. margin: auto;
  192. -moz-box-sizing: border-box;
  193. -webkit-box-sizing: border-box;
  194. width: 100%;
  195. }
  196. .timeMachineControl .datepickerContainer .ui-datepicker .ui-datepicker-title select.ui-datepicker-year {
  197. width: 40%;
  198. }
  199. .timeMachineControl .datepickerContainer .ui-datepicker .ui-datepicker-title select.ui-datepicker-month {
  200. width: 40%;
  201. }
  202. .timeMachineControl .ui-widget-content {
  203. border: none;
  204. }
  205.  
  206. .timeMachineControl .inputFormContainer {
  207. text-align: center;
  208. }
  209. .timeMachineControl .dateInput {
  210. text-align: center;
  211. width: 150px;
  212. }
  213. .timeMachineControl .hourInput:after {
  214. content: ' : ';
  215. font-weight: boler;
  216. }
  217. .timeMachineControl .submitButtonContainer {
  218. text-align: center;
  219. padding: 8px;
  220. }
  221. .timeMachineControl .reset {
  222. display: none;
  223. }
  224. .timeMachineControl.past .reset {
  225. display: inline-block;
  226. }
  227. .timeMachineControl .reset, .timeMachineControl .submit {
  228. padding: 8px;
  229. cursor: pointer;
  230. }
  231.  
  232.  
  233. {* *}
  234. .watchWatchContainer .year:after { content: '/'; }
  235. .watchWatchContainer .month:after { content: '/'; }
  236. .watchWatchContainer .date:after { content: ''; }
  237.  
  238. .watchWatchContainer .day:before {
  239. content: '(';
  240. }
  241. .watchWatchContainer .day:after {
  242. content: ') ';
  243. }
  244.  
  245. .watchWatchContainer.sun .day .inner:after { content: '日'; }
  246. .watchWatchContainer.mon .day .inner:after { content: '月'; }
  247. .watchWatchContainer.tue .day .inner:after { content: '火'; }
  248. .watchWatchContainer.wed .day .inner:after { content: '水'; }
  249. .watchWatchContainer.thu .day .inner:after { content: '木'; }
  250. .watchWatchContainer.fri .day .inner:after { content: '金'; }
  251. .watchWatchContainer.sat .day .inner:after { content: '土'; }
  252.  
  253. .watchWatchContainer .hour:after { content: ':'; }
  254. .watchWatchContainer .min:after { content: ':'; }
  255. .watchWatchContainer .sec:after { content: ''; }
  256.  
  257. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  258.  
  259. this.addStyle(__css__, 'WatchWatchCss');
  260. },
  261. initializeUserConfig: function() {
  262. var prefix = 'WatchWatch_';
  263. var conf = {};
  264. this.config = {
  265. get: function(key) {
  266. try {
  267. if (window.localStorage.hasOwnProperty(prefix + key)) {
  268. return JSON.parse(window.localStorage.getItem(prefix + key));
  269. }
  270. return conf[key];
  271. } catch (e) {
  272. return conf[key];
  273. }
  274. },
  275. set: function(key, value) {
  276. window.localStorage.setItem(prefix + key, JSON.stringify(value));
  277. }
  278. };
  279. },
  280. initializeTimeMachine: function() {
  281. var timeMachine = this._timeMachine;
  282. var watchInfoModel = this._watchInfoModel;
  283. var playerAreaConnector = this._playerAreaConnector;
  284.  
  285. timeMachine.addEventListener('reset', $.proxy(function() {
  286. this._isPastMode = false;
  287. this.event.dispatchEvent('timeMachine-changePastMode', false, new Date());
  288. }, this));
  289. timeMachine.addEventListener('error', $.proxy(function() {
  290. this.event.dispatchEvent('timeMachine.error');
  291. }, this));
  292.  
  293. playerAreaConnector.addEventListener('onTimeMachineDateUpdated', $.proxy(function(isPast, time) {
  294. console.log('dispatch.timeMachine-changePastMode', isPast, time);
  295. if (isPast) {
  296. this._isPastMode = true;
  297. this._pastTime = time;
  298. this.event.dispatchEvent('timeMachine-changePastMode', true, new Date(time));
  299. } else {
  300. this._isPastMode = false;
  301. this.event.dispatchEvent('timeMachine-changePastMode', false, new Date());
  302. }
  303. }, this));
  304.  
  305. watchInfoModel.addEventListener('reset', $.proxy(function() {
  306. this._isPastMode = false;
  307. if (initialized) {
  308. beforeShow();
  309. }
  310. }, this));
  311.  
  312. var beforeShow = $.proxy(function() {
  313. var md = watchInfoModel.postedAt.split(' ')[0].split('/');
  314. var $date = this._$datepickerContainer;
  315. $date.datepicker('option', 'minDate', new Date(md[0], md[1] - 1, md[2]));
  316. $date.datepicker('option', 'maxDate', new Date());
  317. var v = this._$dateInput.val();
  318. if (v.match(/^(\d{4})[¥/-](\d{2})[¥/-](\d{2})$/)) {
  319. var dt = new Date(RegExp.$1, RegExp.$2 - 1, RegExp.$3);
  320. if (dt) $date.datepicker('setDate', dt);
  321. }
  322. }, this);
  323.  
  324. var initialize = $.proxy(function() {
  325. if (initialized) { return; }
  326. this._$datepickerContainer.datepicker({
  327. dateFormat: 'yy/mm/dd',
  328. // beforeShow: beforeShow,
  329. altField: this._$dateInput,
  330. changeMonth: true,
  331. changeYear: true
  332. });
  333.  
  334. initialized = true;
  335. }, this), initialized = false;
  336.  
  337. this.resetDatepicker = $.proxy(function() {
  338. initialize();
  339. beforeShow();
  340. }, this);
  341.  
  342. this.event.addEventListener('popup.toggle', $.proxy(function(v) {
  343. if (!v) { return; }
  344. this.resetDatepicker();
  345. }, this));
  346.  
  347. this.timeMachineController = {
  348. goToPast: function(tm) {
  349. if (!tm) tm = (new Date()).getTime();
  350. if (typeof tm === 'object' && tm.getTime) tm = tm.getTime();
  351.  
  352. tm = Math.min(window.WatchApp.ns.util.TimeUtil.now(), tm);
  353. var postedAt = new Date(watchInfoModel.postedAt.substring(0, 16).replace(/-/g, '/').split(' '));
  354. console.log('postedAt', postedAt.getTime(), tm);
  355. // 投稿直後より前は指定できない。マイメモリーやチャンネルだと怪しいかも
  356. // 投稿日時ぴったりもだめっぽい。 おそらく投稿確定後にスレッドが作られるため。
  357. tm = Math.max(postedAt.getTime() + 300 * 1000, tm);
  358. timeMachine.goToPast(tm);
  359. },
  360. now: function() {
  361. timeMachine.goToPresent();
  362. }
  363. };
  364. },
  365. initializeDom: function() {
  366. $('#playerTabWrapper').addClass('watchWatch');
  367. var $watch = this._$watch = $([
  368. '<div id="watchWatch" class="watchWatchContainer sun">',
  369. '<span class="year">2014</span>',
  370. '<span class="month">01</span>',
  371. '<span class="date">01</span>',
  372. '<span class="day">',
  373. '<span class="inner"></span>',
  374. '</span>',
  375. '<span class="hour">00</span>',
  376. '<span class="min">00</span>',
  377. '<span class="sec">00</span>',
  378. '</div>',
  379. ''].join(''));
  380. var $popup = this._$popup = $([
  381. '<div id="watchWatchTimeMachine" class="timeMachineControl">',
  382. '<h1 class="title">過去のコメントを開く</h1>',
  383. '<div class="datepickerContainer"></div>',
  384. '<div class="inputFormContainer">',
  385. '<input type="text" name="date" value="2014/01/01" class="dateInput">',
  386. '<select name="hour" class="hourInput"></select>',
  387. '<select name="minute" class="minuteInput"></select>',
  388. '</div>',
  389. '',
  390. '<div class="submitButtonContainer">',
  391. '<button class="submit">過去ログを見る</button>',
  392. '<button class="reset">最新ログに戻る</button>',
  393. '</div>',
  394. '</div>',
  395. ''].join(''));
  396. $('#playerCommentPanel, #playerTabContainer .comment-panel').after($watch).after($popup);
  397.  
  398. this._$datepickerContainer = $popup.find('.datepickerContainer');
  399. this._$dateInput = $popup.find('.dateInput');
  400. this._$hourInput = $popup.find('.hourInput');
  401. this._$minuteInput = $popup.find('.minuteInput');
  402. this._$submitButton = $popup.find('.submit');
  403. this._$resetButton = $popup.find('.reset');
  404.  
  405. var options = [];
  406. for (var i = 0; i < 60; i++) {
  407. var m = ('0' + i).slice(-2);
  408. options.push(['<option value="', m, '">', m, '</option>'].join(''));
  409. }
  410. this._$hourInput.html(options.slice(0, 24).join(''));
  411. this._$minuteInput.html(options.join(''));
  412.  
  413. var resetInput = $.proxy(function(time) {
  414. var date;
  415. if (typeof time === 'number') { date = new Date(time); }
  416. else
  417. if (typeof time === 'object' && time.getTime) date = time;
  418. else
  419. if (!time) date = new Date();
  420. console.log('resetInput', time, date);
  421. var y = date.getFullYear();
  422. var m = (date.getMonth() + 101).toString().slice(-2);
  423. var d = ('0' + date.getDate()).slice(-2);
  424.  
  425. var hh = ('0' + date.getHours()).slice(-2);
  426. var mm = ('0' + date.getMinutes()).slice(-2);
  427. this._$dateInput .val(y + '/' + m + '/' + d);
  428. this._$hourInput .val(hh);
  429. this._$minuteInput.val(mm);
  430. //console.log('dt', y, m, d, hh, mm);
  431. }, this);
  432.  
  433. $watch.on('click', $.proxy(function() {
  434. this.togglePopup();
  435. }, this));
  436.  
  437. this.togglePopup = $.proxy(function(v) {
  438. if (typeof v === 'boolean') {
  439. this._$popup.toggleClass('show', v);
  440. } else {
  441. this._$popup.toggleClass('show');
  442. }
  443. resetInput(this.getPastTime());
  444. this.event.dispatchEvent('popup.toggle', this._$popup.hasClass('show'));
  445. }, this);
  446. this.showPopup = $.proxy(function() { this.togglePopup(true); }, this);
  447. this.hidePopup = $.proxy(function() { this.togglePopup(false); }, this);
  448.  
  449. this.event.addEventListener('timeMachine-changePastMode', $.proxy(function(isPast, time) {
  450. this.hidePopup();
  451. console.log('initializeDom.timeMachine-changePastMode', isPast, time);
  452. this._$watch.toggleClass('past', isPast);
  453. this._$popup.toggleClass('past', isPast);
  454. resetInput(time);
  455. }, this));
  456. this.event.addEventListener('timeMachine.error', $.proxy(function() {
  457. this.hidePopup(); // エラーメッセージが見えないので閉じる
  458. }, this));
  459.  
  460. this._$submitButton.on('click', $.proxy(function() {
  461. var dd = this._$dateInput.val().replace(/-/g, '/');
  462. var tt = this._$hourInput.val() + ':' + this._$minuteInput.val();
  463. var dt = new Date(dd + ' ' + tt + ':00');
  464.  
  465. this.timeMachineController.goToPast(dt);
  466. }, this));
  467. this._$resetButton.on('click', $.proxy(function() {
  468. this.timeMachineController.now();
  469. }, this));
  470.  
  471. $watch = $popup = null;
  472. },
  473. initializeTimer: function() {
  474. var date = new window.Date();
  475. var $watch = this._$watch;
  476. var $year = $watch.find('.year');
  477. var $month = $watch.find('.month');
  478. var $date = $watch.find('.date');
  479. var $hour = $watch.find('.hour');
  480. var $min = $watch.find('.min');
  481. var $sec = $watch.find('.sec');
  482. var days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
  483. var timer;
  484.  
  485. var update = function(force, time) {
  486. if (time) {
  487. date.setTime(time);
  488. console.log('setTime', time, date);
  489. } else {
  490. date.setTime((new Date()).getTime());
  491. }
  492. var sec = date.getSeconds();
  493. $sec.text(('0' + sec).slice(-2));
  494.  
  495. if (force || sec === 0) {
  496. var min = date.getMinutes();
  497. $min.text(('0' + min).slice(-2));
  498.  
  499. if (force || min === 0) {
  500. $hour .text(('0' + date.getHours()).slice(-2));
  501. $date .text(('0' + date.getDate()) .slice(-2));
  502. var month = date.getMonth();
  503. $month.text((date.getMonth() + 101).toString().slice(-2));
  504. $year .text(date.getFullYear());
  505. var day = date.getDay();
  506. $watch
  507. .removeClass('month' + (((month + 11) % 12) + 1))
  508. .addClass( 'month' + (month + 1))
  509. .removeClass(days[(day + 6) % 7] )
  510. .addClass( days[day]);
  511. }
  512. }
  513. };
  514. var onTimer = this._onTimer = $.proxy(function() {
  515. if (this._isPastMode) { return; }
  516. update(false);
  517. }, this);
  518.  
  519. this.event.addEventListener('timeMachine-changePastMode', function(isPast, time) {
  520. console.log('timer.timeMachine-changePastMode', isPast, time, new Date(time));
  521. if (isPast) {
  522. window.setTimeout(function() { update(true, time); }, 1000);
  523. } else {
  524. update(true);
  525. }
  526. });
  527.  
  528. update(true);
  529. window.WatchApp.mixin(this, {
  530. watch: {
  531. start: function() {
  532. if (timer) return;
  533. timer = window.setInterval($.proxy(onTimer, this), 600);
  534. },
  535. stop: function() {
  536. window.clearInterval(timer);
  537. timer = null;
  538. },
  539. refresh: function() {
  540. update(true);
  541. }
  542. }
  543. });
  544. this.watch.start();
  545. }
  546. });
  547.  
  548. if (window.PlayerApp) {
  549. require(['WatchApp'], function() {
  550. var watchInfoModel = require('watchapp/model/WatchInfoModel').getInstance();
  551. if (watchInfoModel.initialized) {
  552. window.WatchWatch.initialize();
  553. } else {
  554. var onReset = function() {
  555. watchInfoModel.removeEventListener('reset', onReset);
  556. window.setTimeout(function() {
  557. watchInfoModel.removeEventListener('reset', onReset);
  558. window.WatchWatch.initialize();
  559. }, 0);
  560. };
  561. watchInfoModel.addEventListener('reset', onReset);
  562. }
  563. });
  564. }
  565.  
  566.  
  567. });
  568.  
  569. var script = document.createElement('script');
  570. script.id = 'WatchWatchLoader';
  571. script.setAttribute('type', 'text/javascript');
  572. script.setAttribute('charset', 'UTF-8');
  573. script.appendChild(document.createTextNode('(' + monkey + ')()'));
  574. document.body.appendChild(script);
  575.  
  576. })();