MylistPocket

動画をあとで見る + 簡易NG機能。 ZenzaWatchとの連携も可能。

当前为 2017-05-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MylistPocket
  3. // @namespace https://github.com/segabito/
  4. // @description 動画をあとで見る + 簡易NG機能。 ZenzaWatchとの連携も可能。
  5. // @match http://www.nicovideo.jp/*
  6. // @match http://ext.nicovideo.jp/
  7. // @match http://ext.nicovideo.jp/#*
  8. // @match http://ch.nicovideo.jp/*
  9. // @match http://com.nicovideo.jp/*
  10. // @match http://commons.nicovideo.jp/*
  11. // @match http://dic.nicovideo.jp/*
  12. // @match http://ex.nicovideo.jp/*
  13. // @match http://info.nicovideo.jp/*
  14. // @match http://search.nicovideo.jp/*
  15. // @match http://uad.nicovideo.jp/*
  16. // @exclude http://ads*.nicovideo.jp/*
  17. // @exclude http://www.upload.nicovideo.jp/*
  18. // @exclude http://www.nicovideo.jp/watch/*?edit=*
  19. // @exclude http://ch.nicovideo.jp/tool/*
  20. // @exclude http://flapi.nicovideo.jp/*
  21. // @exclude http://dic.nicovideo.jp/p/*
  22. // @version 0.2.13
  23. // @grant none
  24. // @author segabito macmoto
  25. // @license public domain
  26. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js
  27. // ==/UserScript==
  28.  
  29. // 保留
  30. // https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.23/webcomponents.js
  31.  
  32. (function() {
  33. const PRODUCT = 'MylistPocket';
  34.  
  35. const monkey = function(PRODUCT) {
  36. const console = window.console;
  37. //const $ = window.jQuery;
  38. console.log(`exec ${PRODUCT}..`);
  39. const TOKEN = 'r:' + (Math.random());
  40.  
  41. const CONSTANT = {
  42. BASE_Z_INDEX: 100000
  43. };
  44. const MylistPocket = {debug: {}};
  45. window.MylistPocket = MylistPocket;
  46.  
  47. const __css__ = (`
  48. a[href*='watch/'] {
  49. display: inline-block;
  50. }
  51.  
  52. .mylistPocketHoverMenu {
  53. display: none;
  54. opacity: 0.8;
  55. position: absolute;
  56. z-index: ${CONSTANT.BASE_Z_INDEX + 100000};
  57. font-size: 8pt;
  58. padding: 0;
  59. line-height: 26px;
  60. font-weight: bold;
  61. text-align: center;
  62. transition: box-shadow 0.2s ease, opacity 0.4s ease, padding 0.2s ease;
  63. user-select: none;
  64. -webkit-user-select: none;
  65. -moz-user-select: none;
  66. }
  67.  
  68. .mylistPocketHoverMenu.is-busy {
  69. opacity: 0 !important;
  70. pointer-events: none;
  71. }
  72. .mylistPocketHoverMenu.is-otherDomain .wwwOnly {
  73. display: none;
  74. }
  75. .mylistPocketHoverMenu.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
  76. display: none;
  77. }
  78. .mylistPocketHoverMenu .zenzaMenu {
  79. display: none;
  80. }
  81. .mylistPocketHoverMenu.is-zenzaReady .zenzaMenu {
  82. display: inline-block;
  83. }
  84.  
  85.  
  86. .mylistPocketButton {
  87. /*font-family: Menlo;*/
  88. display: block;
  89. font-weight: bolder;
  90. cursor: pointer;
  91. width: 32px;
  92. height: 26px;
  93. cursor: pointer;
  94. box-shadow: 1px 1px 1px #000;
  95. transition:
  96. 0.1s box-shadow ease,
  97. 0.1s transform ease;
  98. font-size: 16px;
  99. line-height: 24px;
  100. -webkit-user-select: none;
  101. -moz-use-select: none;
  102. user-select: none;
  103. outline: none;
  104. }
  105.  
  106. .mylistPocketButton:hover {
  107. transform: scale(1.2);
  108. box-shadow: 4px 4px 5px #000;
  109. }
  110.  
  111. .mylistPocketButton:active {
  112. transform: scale(1.0);
  113. box-shadow: none;
  114. transition: none;
  115. }
  116.  
  117. .is-deflistUpdating .mylistPocketButton.deflist-add::after,
  118. .is-deflistSuccess .mylistPocketButton.deflist-add::after,
  119. .is-deflistFail .mylistPocketButton.deflist-add::after,
  120. .mylistPocketButton:hover::after, #mylistPocket-poupup [tooltip] {
  121. content: attr(tooltip);
  122. position: absolute;
  123. /*top: 0px;
  124. left: 50%;*/
  125. top: 50%;
  126. right: -8px;
  127. padding: 2px 4px;
  128. white-space: nowrap;
  129. font-size: 12px;
  130. color: #fff;
  131. background: #333;
  132. transform: translate3d(-50%, -120%, 0);
  133. transform: translate3d(100%, -50%, 0);
  134. pointer-events: none;
  135. }
  136.  
  137. .is-deflistUpdating .mylistPocketButton.deflist-add {
  138. cursor: wait;
  139. opacity: 0.9;
  140. transform: scale(1.0);
  141. box-shadow: none;
  142. transition: none;
  143. background: #888;
  144. border-style: inset;
  145. }
  146. .is-deflistSuccess .mylistPocketButton.deflist-add,
  147. .is-deflistFail .mylistPocketButton.deflist-add {
  148. transform: scale(1.0);
  149. box-shadow: none;
  150. transition: none;
  151. }
  152. .is-deflistSuccess .mylistPocketButton.deflist-add::after {
  153. content: attr(data-result);
  154. background: #393;
  155. }
  156. .is-deflistFail .mylistPocketButton.deflist-add::after {
  157. content: attr(data-result);
  158. background: #933;
  159. }
  160. .is-deflistUpdating .mylistPocketButton.deflist-add::after {
  161. content: '更新中';
  162. background: #333;
  163. }
  164.  
  165. .mylistPocketButton + .mylistPocketButton {
  166. margin-top: 4px;
  167. }
  168.  
  169. .mylistPocketHoverMenu:hover {
  170. font-weibht: bolder;
  171. opacity: 1;
  172. }
  173.  
  174. .mylistPocketHoverMenu:active {
  175. }
  176.  
  177. .mylistPocketHoverMenu.is-show {
  178. display: block;
  179. }
  180.  
  181. #mylistPocket-popup {
  182. display: none;
  183. perspective: 800px;
  184. }
  185. #mylistPocket-popup.is-firefox {
  186. /*perspective: none !important;*/
  187. position: fixed;
  188. z-index: 200000;
  189. transform: translate3d(-50%, -50%, 0);
  190. opacity: 0;
  191. transition: 0.3s opacity ease;
  192. top: -9999px; left: -9999px;
  193. }
  194.  
  195. #mylistPocket-popup.show {
  196. display: block;
  197. }
  198. #mylistPocket-popup.is-firefox.show {
  199. top: 50%;
  200. left: 50%;
  201. opacity: 1;
  202. }
  203.  
  204.  
  205. #mylistPocket-popup .owner-icon {
  206. width: 64px;
  207. height: 64px;
  208. transform-origin: center;
  209. transform-origin: center;
  210. transition:
  211. 0.2s transform ease,
  212. 0.2s box-shadow ease
  213. ;
  214. }
  215. #mylistPocket-popup .owner-icon:hover {
  216. }
  217.  
  218. #mylistPocket-popup .description a {
  219. color: #ffff00 !important;
  220. text-decoration: none !important;
  221. font-weight: normal !important;
  222. display: inline-block;
  223. }
  224. #mylistPocket-popup .description a.watch {
  225. display: block;
  226. }
  227.  
  228. #mylistPocket-popup .description a:visited {
  229. color: #ffff99 !important;
  230. }
  231. #mylistPocket-popup .description button {
  232. /*font-family: Menlo;*/
  233. font-size: 16px;
  234. font-weight: bolder;
  235. margin: 4px 8px;
  236. padding: 4px 8px;
  237. cursor: pointer;
  238. border-radius: 0;
  239. background: #333;
  240. color: #ccc;
  241. border: solid 2px #ccc;
  242. outline: none;
  243. }
  244. #mylistPocket-popup .description button:hover {
  245. transform: translate(-2px,-2px);
  246. box-shadow: 2px 2px 2px #000;
  247. background: #666;
  248. transition:
  249. 0.2s transform ease,
  250. 0.2s box-shadow ease
  251. ;
  252. }
  253. #mylistPocket-popup .description button:active {
  254. transform: none;
  255. box-shadow: none;
  256. transition: none;
  257. }
  258. #mylistPocket-popup .description button:active::hover {
  259. opacity: 0;
  260. }
  261.  
  262. #mylistPocket-popup .watch {
  263. display: block;
  264. position: relative;
  265. line-height: 60px;
  266. box-sizing: border-box;
  267. padding: 4px 16px;;
  268. min-height: 60px;
  269. width: 280px;
  270. margin: 8px 10px;
  271. background: #444;
  272. border-radius: 4px;
  273. }
  274.  
  275. #mylistPocket-popup .watch:hover {
  276. background: #446;
  277. }
  278.  
  279. #mylistPocket-popup .videoThumbnail {
  280. position: absolute;
  281. right: 16px;
  282. height: 60px;
  283. transform-origin: center;
  284. transition:
  285. 0.2s transform ease,
  286. 0.2s box-shadow ease
  287. ;
  288. }
  289. #mylistPocket-popup .videoThumbnail:hover {
  290. transform: scale(2);
  291. box-shadow: 0 0 8px #888;
  292. transition:
  293. 0.2s transform ease 0.5s,
  294. 0.2s box-shadow ease 0.5s
  295. ;
  296. }
  297.  
  298.  
  299. .zenzaPlayerContainer.is-error #mylistPocket-popup,
  300. .zenzaPlayerContainer.is-loading #mylistPocket-popup,
  301. .zenzaPlayerContainer.error #mylistPocket-popup,
  302. .zenzaPlayerContainer.loading #mylistPocket-popup {
  303. opacity: 0;
  304. pointer-events: none;
  305. }
  306.  
  307. `).trim();
  308.  
  309. const __tpl__ = (`
  310. <div class="mylistPocketHoverMenu scalingUI">
  311. <button class="mylistPocketButton command deflist-add wwwZenzaOnly" data-command="deflist"
  312. tooltip="とりあえずマイリスト">&#x271A;</button>
  313. <button class="mylistPocketButton command info" data-command="info"
  314. tooltip="動画情報を表示">?</button>
  315. <button class="mylistPocketButton command playlist-queue zenzaMenu" data-command="playlist-queue"
  316. tooltip="ZenzaWatchのプレイリストに追加">▶</button>
  317. </div>
  318. </div>
  319.  
  320. <div id="mylistPocket-popup">
  321. <span slot="video-title">【実況】どんぐりころころの大冒険 Part1(最終回)</span>
  322. <a href="/watch/sm9" slot="watch-link"></a>
  323. <img slot="video-thumbnail" data-type="image">
  324. <a slot="owner-page-link" href="//www.nicovideo.jp/user/1234" class="owner-page-link target-change" data-type="link" rel="noopener"><img slot="owner-icon" class="owner-icon" src="http://res.nimg.jp/img/user/thumb/blank_s.jpg" data-type="image"></img></a>
  325.  
  326. <span slot="upload-date" data-type="date">1970/01/01 00:00</span>
  327. <span slot="view-counter" data-type="int">12,345</span>
  328. <span slot="mylist-counter" data-type="int">6,789</span>
  329. <span slot="comment-counter" data-type="int">2,525</span>
  330.  
  331. <span slot="duration" class="duration">1:23</span>
  332.  
  333. <span slot="owner-id">1234</span>
  334. <span slot="locale-owner-name">ほげほげ</span>
  335.  
  336. <div slot="error-description"></div>
  337. <div class="description" slot="description" data-type="html"></div>
  338. <span slot="last-res-body"></span>
  339.  
  340. </div>
  341.  
  342. <template id="mylistPocket-popup-template">
  343. <style>
  344.  
  345. :host(#mylistPocket-popup) {
  346. position: fixed;
  347. z-index: 200000;
  348. transform: translate3d(-50%, -50%, 0);
  349. opacity: 0;
  350. transition: 0.3s opacity ease;
  351. top: -9999px; left: -9999px;
  352. }
  353.  
  354. :host(#mylistPocket-popup.show) {
  355. top: 50%;
  356. left: 50%;
  357. opacity: 1;
  358. }
  359.  
  360. .root.is-otherDomain .wwwOnly {
  361. display: none;
  362. }
  363. .root.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
  364. display: none;
  365. }
  366.  
  367. * {
  368. box-sizing: border-box;
  369. font-kerning: none;
  370. }
  371.  
  372. a {
  373. color: #ffff00;
  374. font-weight: bold;
  375. display: inline-block;
  376. }
  377.  
  378. a:visited {
  379. color: #ffff99;
  380. }
  381.  
  382. button {
  383. font-size: 14px;
  384. padding: 8px 8px;
  385. cursor: pointer;
  386. border-radius: 0;
  387. margin: 0;
  388. background: #333;
  389. color: #ccc;
  390. border: solid 2px #ccc;
  391. outline: none;
  392. line-height: 20px;
  393. user-select: none;
  394. -webkit-user-select: none;
  395. -moz-user-select: none;
  396. }
  397. button:hover {
  398. transform: translate(-4px,-4px);
  399. box-shadow: 4px 4px 4px #000;
  400. background: #666;
  401. transition:
  402. 0.2s transform ease,
  403. 0.2s box-shadow ease
  404. ;
  405. }
  406.  
  407. button.is-updating {
  408. cursor: wait;
  409. }
  410. button.is-active,
  411. button:active {
  412. transform: none;
  413. box-shadow: none;
  414. transition: none;
  415. }
  416. button.is-active::after,
  417. button:active::after {
  418. opacity: 0;
  419. }
  420.  
  421.  
  422. [tooltip] {
  423. position: relative;
  424. }
  425.  
  426. .is-deflistUpdating .deflist-add::after,
  427. .is-deflistSuccess .deflist-add::after,
  428. .is-deflistFail .deflist-add::after,
  429. [tooltip]:hover::after {
  430. content: attr(tooltip);
  431. position: absolute;
  432. top: 0px;
  433. left: 50%;
  434. padding: 2px 4px;
  435. white-space: nowrap;
  436. font-size: 14px;
  437. color: #fff;
  438. background: #333;
  439. transform: translate3d(-50%, -120%, 0);
  440. pointer-events: none;
  441.  
  442. }
  443.  
  444.  
  445. .root {
  446. text-align: left;
  447.  
  448. border-radius: 0px;
  449. outline-offset: 8px;
  450. border: 12px solid rgba(32, 32, 32, 0);
  451. border-radius: 20px;
  452. padding: 8px 0;
  453. background: rgba(0, 0, 0, 0.7);
  454. color: #ccc;
  455. box-shadow: 0 0 16px #000;
  456. transition:
  457. 0.6s -webkit-clip-path ease,
  458. 0.6s clip-path ease,
  459. 0.5s transform ease;
  460. /*0.4s border-radius ease-out 0.4s,
  461. 0.4s height ease-out 0.4s*/
  462. ;
  463. }
  464.  
  465. .root * {
  466. }
  467.  
  468. .root.show {
  469. opacity: 1;
  470. }
  471.  
  472. .root.is-loading,
  473. .root.is-loading.is-ok,
  474. .root.is-loading.is-fail {
  475. text-align: center;
  476. position: relative;
  477. width: 190px;
  478. height: 190px;
  479. padding: 32px;
  480. opacity: 0.8;
  481. cursor: wait;
  482. border-radius: 100%;
  483. clip-path: circle(100px at center) !important;
  484. transition: none;
  485. outline: none;
  486. transform: none !important;
  487. }
  488. .root.is-firefox {
  489. }
  490. .root.is-loading > * {
  491. pointer-events: none;
  492. }
  493.  
  494. .root.is-setting {
  495. transform: rotateX(180deg);
  496. }
  497.  
  498. .root.is-setting > *:not(.setting-panel) {
  499. pointer-events: none;
  500. z-index: 1;
  501. }
  502.  
  503. .root:not(.is-setting) > .setting-panel {
  504. pointer-events: none;
  505. }
  506.  
  507. .root.is-setting > .setting-panel {
  508. display: block;
  509. opacity: 1;
  510. pointer-events: auto;
  511. }
  512.  
  513. .root.is-loading .loading-inner,
  514. .root.is-loading.is-ok .loading-inner,
  515. .root.is-loading.is-fail .loading-inner {
  516. position: absolute;
  517. top: 50%;
  518. left: 50%;
  519. transform: translate3d(-50%, -50%, 0);
  520. }
  521.  
  522. .loading-inner .spinner {
  523. font-size: 64px;
  524. display: inline-block;
  525. animation-name: spin;
  526. animation-iteration-count: infinite;
  527. animation-duration: 3s;
  528. animation-timing-function: linear;
  529. }
  530.  
  531. @keyframes spin {
  532. 0% { transform: rotate(0deg); }
  533. 100% { transform: rotate(1800deg); }
  534. }
  535.  
  536.  
  537.  
  538. .root.is-ok {
  539. width: 800px;
  540. clip-path: circle(800px at center);
  541. }
  542.  
  543. .root.is-ok.noclip {
  544. clip-path: none;
  545. }
  546.  
  547. .root.is-fail {
  548. font-size: 120%;
  549. white-space: nowrap;
  550. text-align: center;
  551. padding: 16px;
  552. }
  553.  
  554. .root.is-loading>*:not(.loading-now),
  555. .root.is-loading.is-ok>*:not(.loading-now),
  556. .root.is-loading.is-fail>*:not(.loading-now),
  557. .root.is-fail:not(.is-loading)>*:not(.error-info),
  558. .root.is-ok:not(.is-loading)>*:not(.video-detail):not(.setting-panel) {
  559. display: none !important;
  560. }
  561.  
  562. .root.is-loading>.loading-now,
  563. .root.is-fail>.error-info,
  564. .root.is-ok>.video-detail {
  565. display: block;
  566. }
  567.  
  568. .header {
  569. padding: 8px 8px 8px;
  570. font-size: 12px;
  571. }
  572. .upload-date {
  573. margin-right: 8px;
  574. }
  575. .counter span + span {
  576. margin-left: 8px;
  577. }
  578. .video-title {
  579. font-weight: bolder;
  580. font-size: 22px;
  581. margin-bottom: 4px;
  582. }
  583.  
  584.  
  585.  
  586. .main {
  587. display: flex;
  588. background: rgba(0, 0, 0, 0.2);
  589. box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset;
  590. }
  591.  
  592. .main-left {
  593. width: 360px;
  594. padding: 8px;
  595. z-index: 100;
  596. }
  597. .video-thumbnail-container {
  598. position: relative;
  599. width: 360px;
  600. height: 270px;
  601. background: #000;
  602. /*box-shadow: 2px 2px 4px #000;*/
  603. }
  604. .video-thumbnail-container ::slotted(img) {
  605. width: 360px;
  606. height: 270px;
  607. }
  608.  
  609. .video-thumbnail-container .duration {
  610. position: absolute;
  611. display: inline-block;
  612. right: 0;
  613. bottom: 0;
  614. font-size: 14px;
  615. background: #000;
  616. color: #fff;
  617. padding: 2px 4px;
  618. }
  619. .video-thumbnail-container:hover .duration {
  620. display: none;
  621. }
  622.  
  623.  
  624. .main-right {
  625. position: relative;
  626. padding: 0;
  627. flex-grow: 1;
  628. font-size: 14px;
  629. }
  630.  
  631. ::slotted(.owner-page-link) {
  632. display: inline-block;
  633. vertical-align: middle;
  634. }
  635.  
  636. .owner-page-link img {
  637. border: 1px solid #333;
  638. border-radius: 3px;
  639. }
  640.  
  641. .video-info {
  642. /*background: rgba(0, 0, 0, 0.2);*/
  643. max-height: 282px;
  644. overflow-x: hidden;
  645. overflow-y: scroll;
  646. }
  647.  
  648. *::-webkit-scrollbar,
  649. .video-info::-webkit-scrollbar {
  650. background: rgba(34, 34, 34, 0.5);
  651. }
  652.  
  653. *::-webkit-scrollbar-thumb,
  654. .video-info::-webkit-scrollbar-thumb {
  655. border-radius: 0;
  656. background: #666;
  657. }
  658.  
  659. *::-webkit-scrollbar-button,
  660. .video-info::-webkit-scrollbar-button {
  661. background: #666;
  662. display: none;
  663. }
  664.  
  665. *::scrollbar,
  666. .video-info::scrollbar {
  667. background: #222;
  668. }
  669.  
  670. *::scrollbar-thumb,
  671. .video-info::scrollbar-thumb {
  672. border-radius: 0;
  673. background: #666;
  674. }
  675.  
  676. *::scrollbar-button,
  677. .video-info::scrollbar-button {
  678. background: #666;
  679. display: none;
  680. }
  681.  
  682.  
  683.  
  684. .owner-info {
  685. margin: 16px;
  686. display: inline-block;
  687. }
  688.  
  689. .owner-info * {
  690. vertical-align: middle;
  691. }
  692. .owner-name {
  693. display: inline-block;
  694. padding: 8px;
  695. font-size: 18px;
  696. }
  697. .owner-info.is-favorited {
  698. font-weight: bolder;
  699. color: orange;
  700. }
  701.  
  702. .owner-info.is-ng {
  703. color: #888;
  704. text-decoration: line-through;
  705. }
  706.  
  707. .is-channel .owner-name::before {
  708. content: 'CH';
  709. margin: 0 4px;
  710. background: #999;
  711. color: #333;
  712. padding: 2px 4px;
  713. border: 1px solid;
  714. }
  715.  
  716. .locale-owner-name::after {
  717. content: ' さん';
  718. }
  719.  
  720. .owner-info .add-ng-button,
  721. .owner-info .add-fav-button {
  722. visibility: hidden;
  723. pointer-events: none;
  724. }
  725. .is-ng-enable .owner-info:hover .add-ng-button,
  726. .is-ng-enable .owner-info:hover .add-fav-button {
  727. visibility: visible;
  728. pointer-events: auto;
  729. }
  730.  
  731. .description {
  732. word-break: break-all;
  733. line-height: 1.5;
  734. padding: 0 16px 8px;
  735. }
  736.  
  737. .description:first-letter {
  738. font-size: 24px;
  739. }
  740. .last-res-body {
  741. margin: 16px 16px 0;
  742. border: 1px solid #ccc;
  743. padding: 4px;
  744. border-radius: 4px;
  745. word-break: break-all;
  746. font-size: 12px;
  747. min-height: 24px;
  748. }
  749.  
  750.  
  751. .footer {
  752. padding: 8px;
  753. }
  754.  
  755. .pocket-button {
  756. cusror: pointer;
  757. }
  758.  
  759. .pocket-button:active {
  760. }
  761.  
  762.  
  763. .video-tags {
  764. display: block;
  765. }
  766.  
  767. .tag-container {
  768. display: inline-block;
  769. position: relative;
  770. padding: 4px 8px;
  771. border: 1px solid #888;
  772. border-radius: 4px;
  773. margin: 0 20px 4px 0;
  774. }
  775. .tag-container .tag {
  776. display: inline-block;
  777. font-size: 14px;
  778. color: #ccc;
  779. text-decoration: none;
  780. cursor: pointer;
  781. }
  782. .tag-container:hover .tag {
  783. color: #fff !important;
  784. }
  785. .tag-container.is-favorited .tag {
  786. font-weight: bolder;
  787. color: orange !important;
  788. }
  789. .tag-container.is-ng .tag {
  790. text-decoration: line-through;
  791. color: #888 !important;
  792. }
  793.  
  794.  
  795. .tag-container .add-ng-button,
  796. .tag-container .add-fav-button {
  797. position: absolute !important;
  798. visibility: hidden;
  799. pointer-events: none;
  800. }
  801. .is-ng-enable .tag-container:hover .add-ng-button,
  802. .is-ng-enable .tag-container:hover .add-fav-button {
  803. visibility: visible;
  804. pointer-events: auto;
  805. width: 24px;
  806. height: 24px;
  807. line-height: 24px;
  808. font-size: 24px;
  809. vertical-align: bottom;
  810. display: inline-block;
  811. }
  812. .is-ng-enable .tag-container:hover .add-ng-button {
  813. right: -16px;
  814. }
  815. .is-ng-enable .tag-container:hover .add-fav-button {
  816. left: -16px;
  817. }
  818. .footer-menu {
  819. position: absolute;
  820. right: 0px;
  821. bottom: 0px;
  822. transform: translate3d(0, 120%, 0);
  823. opacity: 1;
  824. transition:
  825. 0.4s opacity ease 0.4s,
  826. 0.4s transform ease 0.4s;
  827. }
  828.  
  829. .is-setting .video-detail .footer-menu {
  830. transform: translate3d(0, 0, 0);
  831. opacity: 0;
  832. }
  833.  
  834. .footer-menu button {
  835. min-width: 70px;
  836. }
  837. .regular-menu {
  838. display: inline-block;
  839. background: rgba(0, 0, 0, 0.7);
  840. position: relative;
  841. border-radius: 8px;
  842. padding: 12px 16px;
  843. box-shadow: 0 0 16px #000;
  844. }
  845.  
  846. .is-deflistUpdating .deflist-add {
  847. cursor: wait;
  848. opacity: 0.9;
  849. transform: scale(1.0);
  850. box-shadow: none;
  851. transition: none;
  852. }
  853. .is-deflistSuccess .deflist-add,
  854. .is-deflistFail .deflist-add {
  855. transform: scale(1.0);
  856. box-shadow: none;
  857. transition: none;
  858. }
  859. .is-deflistSuccess .deflist-add::after {
  860. content: attr(data-result);
  861. background: #393;
  862. }
  863. .is-deflistFail .deflist-add::after {
  864. content: attr(data-result);
  865. background: #933;
  866. }
  867. .is-deflistUpdating .deflist-add::after {
  868. content: '更新中';
  869. background: #333;
  870. }
  871.  
  872. .zenza-menu {
  873. display: none;
  874. }
  875.  
  876. .is-zenzaReady .zenza-menu {
  877. display: inline-block;
  878. background: rgba(0, 0, 0, 0.7);
  879. margin-left: 32px;
  880. position: relative;
  881. border-radius: 8px;
  882. padding: 12px 16px;
  883. box-shadow: 0 0 16px #000;
  884. }
  885.  
  886. .is-zenzaReady .zenza-menu::after {
  887. content: 'ZenzaWatch';
  888. position: absolute;
  889. left: 50%;
  890. bottom: 10px;
  891. padding: 2px 8px;
  892. transform: translate(-50%, 100%);
  893. pointer-events: none;
  894. font-weith: bolder;
  895. background: rgba(0, 0, 0, 0.7);
  896. pointer-events: none;
  897. border-radius: 4px;
  898. white-space: nowrap;
  899. }
  900.  
  901. .setting-menu {
  902. display: inline-block;
  903. background: rgba(0, 0, 0, 0.7);
  904. margin-left: 32px;
  905. position: relative;
  906. border-radius: 8px;
  907. padding: 12px 16px;
  908. box-shadow: 0 0 16px #000;
  909. }
  910.  
  911. .toggle-setting-button {
  912. font-size: 32px;
  913. border-radius: 100%;
  914. border: 12px solid #333;
  915. cursor: pointer;
  916. background: rgba(32, 32, 32, 1);
  917. transition:
  918. 0.2s transform ease
  919. ;
  920. }
  921.  
  922. .toggle-setting-button:hover {
  923. transform: scale(1.2);
  924. box-shadow: none;
  925. background: rgba(32, 32, 32, 1);
  926. background: transparent;
  927. }
  928.  
  929. .toggle-setting-button:active {
  930. transform: scale(1.0);
  931. }
  932.  
  933. .setting-panel {
  934. opacity: 0;
  935. position: absolute;
  936. top: 0;
  937. left: 0;
  938. width: 100%;
  939. height: 100%;
  940. padding: 8px 12px;
  941. z-index: 10000;
  942. background: rgba(50, 50, 64, 0.9);
  943. border-radius: 16px;
  944. color: #ccc;
  945. /*-webkit-user-select: none;
  946. user-select: none;*/
  947. transform: rotateX(180deg);
  948. transition: 0.25s opacity ease 0.25s;
  949. }
  950. .is-setting .setting-panel {
  951. transition: 0.25s opacity ease;
  952. }
  953. .setting-panel-main {
  954. width: 100%;
  955. height: 100%;
  956. overflow-y: scroll;
  957. overflow-x: hidden;
  958. }
  959.  
  960. .root:not(.is-setting) .setting-panel .footer-menu {
  961. transform: translate3d(0, 0, 0);
  962. opacity: 0;
  963. }
  964.  
  965. .root.is-setting .setting-panel .footer-menu {
  966. right: -12px;
  967. bottom: -12px;
  968. transform: translate3d(0, 120%, 0);
  969. opacity: 1;
  970. transition:
  971. 0.4s opacity ease 0.4s,
  972. 0.4s transform ease 0.4s;
  973. }
  974.  
  975.  
  976. .close-setting-menu {
  977. display: inline-block;
  978. background: rgba(0, 0, 0, 0.7);
  979. margin-left: 32px;
  980. position: relative;
  981. border-radius: 8px;
  982. padding: 12px 16px;
  983. box-shadow: 0 0 16px #000;
  984. }
  985. .setting-label {
  986. display: inline-block;
  987. line-height: 24px;
  988. padding: 8px;
  989. }
  990.  
  991. .setting-label:hover {
  992. text-shadow: 0 0 4px #996;
  993. }
  994.  
  995. .setting-label * {
  996. cursor: pointer;
  997. }
  998.  
  999. .setting-label input[type=checkbox] {
  1000. transform: scale(2);
  1001. margin: 8px;
  1002. vertical-align: middle;
  1003. }
  1004.  
  1005. .setting-label input + span {
  1006. font-size: 16px;
  1007. }
  1008.  
  1009. .setting-label input:checked + span {
  1010. }
  1011.  
  1012.  
  1013. .setting-fav,
  1014. .setting-ng-textarea,
  1015. .setting-fav-textarea {
  1016. display: none;
  1017. }
  1018.  
  1019. .is-ng-enable .setting-fav {
  1020. display: block;
  1021. }
  1022. .is-ng-enable .setting-ng-textarea,
  1023. .is-ng-enable .setting-fav-textarea {
  1024. display: flex;
  1025. }
  1026.  
  1027. .setting-ng-text-column,
  1028. .setting-fav-text-column {
  1029. flex: 1;
  1030. position: relative;
  1031. padding: 8px;
  1032. }
  1033.  
  1034. .setting-ng-text-column textarea,
  1035. .setting-fav-text-column textarea {
  1036. width: 100%;
  1037. height: 150px;
  1038. background: transparent;
  1039. color: #ccc;
  1040. }
  1041.  
  1042. .setting-ng-label {
  1043. display: none;
  1044. }
  1045.  
  1046. .is-ng-enable .setting-ng-label {
  1047. display: inline-block;
  1048. }
  1049.  
  1050.  
  1051. .add-ng-button,
  1052. .add-fav-button {
  1053. display: none;
  1054. }
  1055.  
  1056. .is-ng-enable .add-ng-button,
  1057. .is-ng-enable .add-fav-button {
  1058. display: inline-block;
  1059. position: relative;
  1060. width: 32px;
  1061. height: 32px;
  1062. line-height: 32px;
  1063. font-size: 28px;
  1064. padding: 0;
  1065. margin: 0;
  1066. /*border-radius: 100%;*/
  1067. border: none;
  1068. text-align: center;
  1069. color: red;
  1070. font-weight: bolder;
  1071. cursor: pointer;
  1072. background: transparent;
  1073. box-shadow: none;
  1074. transition:
  1075. 0.2s transform ease,
  1076. 0.2s text-shadow ease;
  1077. }
  1078. .is-ng-enable .add-fav-button {
  1079. color: orange;
  1080. }
  1081. .is-ng-enable .add-ng-button:hover,
  1082. .is-ng-enable .add-fav-button:hover {
  1083. transform: scale(1.2);
  1084. text-shadow: 2px 2px 4px black;
  1085. }
  1086. .is-ng-enable .add-ng-button:active,
  1087. .is-ng-enable .add-fav-button:active {
  1088. transform: scale(1.0);
  1089. text-shadow: 0 0 2px black;
  1090. }
  1091. .is-ng-enable .add-ng-button:hover::after,
  1092. .is-ng-enable .add-fav-button:hover::after {
  1093. content: 'NG登録';
  1094. position: absolute;
  1095. top: 0;
  1096. left: 50%;
  1097. transform: translate(-50%, -80%);
  1098. font-size: 12px;
  1099. line-height: 12px;
  1100. white-space: nowrap;
  1101. background: rgba(192, 192, 192, 0.8);
  1102. color: #000;
  1103. opacity: 0.9;
  1104. padding: 2px 4px;
  1105. text-shadow: none;
  1106. font-weight: normal;
  1107. pointer-evnets: none !important;
  1108. }
  1109. .is-ng-enable .is-ng .add-ng-button:hover::after,
  1110. .is-ng-enable .is-ng .add-fav-button:hover::after {
  1111. content: 'NG解除';
  1112. }
  1113. .is-ng-enable .add-fav-button:hover::after {
  1114. content: '強調登録';
  1115. }
  1116. .is-ng-enable .is-favorited .add-fav-button:hover::after {
  1117. content: '強調解除';
  1118. }
  1119. .is-ng-enable .add-ng-button:active:hover::after,
  1120. .is-ng-enable .add-fav-button:active:hover::after {
  1121. display: none;
  1122. }
  1123.  
  1124. </style>
  1125. <div class="popup root">
  1126. <div class="loading-now">
  1127. <div class="loading-inner">
  1128. <span class="spinner">&#8987;</span>
  1129. </div>
  1130. </div>
  1131. <div class="error-info">
  1132. <slot name="error-description"></slot>
  1133. </div>
  1134. <div class="video-detail">
  1135. <div class="header">
  1136. <div class="video-title"><slot name="video-title"></slot></div>
  1137.  
  1138. <span class="upload-date">投稿: <slot name="upload-date"/></span>
  1139. <span class="counter">
  1140. <span class="view-counter">再生: <slot name="view-counter"/></span>
  1141. <span class="mylist-counter">マイリスト: <slot name="mylist-counter"/></span>
  1142. <span class="comment-counter">コメント: <slot name="comment-counter"/></span>
  1143. </span>
  1144. </div>
  1145.  
  1146. <div class="main">
  1147.  
  1148. <div class=" main-left">
  1149. <div class="video-thumbnail-container">
  1150. <slot name="video-thumbnail"></slot>
  1151. <span class="duration"><slot name="duration"></slot></slot>
  1152. </div>
  1153. </div>
  1154.  
  1155. <div class="video-info main-right scrollable">
  1156.  
  1157. <div class="owner-info">
  1158. <slot name="owner-page-link"></slot>
  1159. <span class="owner-name"><slot name="locale-owner-name"></slot>
  1160. <button class="add-fav-button command" data-command="toggle-fav-owner">★</button>
  1161. <button class="add-ng-button command" data-command="toggle-ng-owner">&#x2716;</button>
  1162. </span>
  1163. </div>
  1164.  
  1165. <div class="description">
  1166. <slot name="description"></slot>
  1167. </div>
  1168.  
  1169. <div class="last-res-body">
  1170. <slot name="last-res-body"></slot>
  1171. </div>
  1172.  
  1173. </div>
  1174.  
  1175. </div>
  1176.  
  1177. <div class="footer">
  1178. <div class="video-tags">
  1179. <slot name="tag"></slot>
  1180. </div>
  1181. </div>
  1182. <div class="footer-menu scalingUI">
  1183. <div class="regular-menu">
  1184. <button
  1185. class="mylistPocketButton deflist-add pocket-button command command-watch-id wwwZenzaOnly"
  1186. data-command="deflist-add"
  1187. tooltip="とりあえずマイリスト"
  1188. >とり</button>
  1189. <button
  1190. class="pocket-button command command-watch-id"
  1191. data-command="mylist-window"
  1192. tooltip="マイリスト"
  1193. >マイ</button>
  1194. <button
  1195. class="pocket-button command command-video-id"
  1196. data-command="twitter-hash-open"
  1197. tooltip="Twitterの反応"
  1198. >#Twitter</button>
  1199. </div>
  1200.  
  1201.  
  1202. <div class="zenza-menu">
  1203. <button
  1204. class="pocket-button command command-watch-id"
  1205. data-command="zenza-open-now"
  1206. tooltip="ZenzaWatchで開く"
  1207. >Zen</button>
  1208. <button
  1209. class="pocket-button command command-watch-id"
  1210. data-command="playlist-inert"
  1211. tooltip="プレイリスト(次に再生)"
  1212. >playlist</button>
  1213. <button
  1214. class="pocket-button command command-watch-id"
  1215. data-command="playlist-queue"
  1216. tooltip="プレイリスト(末尾に追加)"
  1217. >queue</button>
  1218. </div>
  1219.  
  1220. <div class="setting-menu">
  1221. <button
  1222. class="pocket-button command"
  1223. data-command="toggle-setting"
  1224. >設 定</button>
  1225. </div>
  1226.  
  1227. </div>
  1228. </div>
  1229. <div class="setting-panel">
  1230.  
  1231. <div class="setting-panel-main scrollable">
  1232. <h2>MylistPocket 設定</h2>
  1233. <label class="setting-label">
  1234. <input
  1235. type="checkbox"
  1236. class="setting-form"
  1237. data-config-name="openNewWindow"
  1238. >
  1239. <span>タグやリンクを新しいタブで開く (次回から反映)</span>
  1240. </label>
  1241.  
  1242. <h2>NG設定(リロード後に反映)</h2>
  1243. <label class="setting-label">
  1244. <input
  1245. type="checkbox"
  1246. class="setting-form"
  1247. data-config-name="enable"
  1248. data-config-namespace="ng"
  1249. >
  1250. <span>簡易NG&強調機能を使う</span>
  1251. </label>
  1252.  
  1253. <div class="setting-ng-textarea setting-ng">
  1254. <div class="setting-ng-text-column">
  1255. 投稿者ID
  1256. <textarea
  1257. class="setting-form"
  1258. data-config-name="owner"
  1259. data-config-namespace="ng"
  1260. ></textarea>
  1261. </div>
  1262. <div class="setting-ng-text-column">
  1263. タグ
  1264. <textarea
  1265. class="setting-form"
  1266. data-config-name="tag"
  1267. data-config-namespace="ng"
  1268. ></textarea>
  1269. </div>
  1270. <div class="setting-ng-text-column">
  1271. タイトル・説明文
  1272. <textarea
  1273. class="setting-form"
  1274. data-config-name="word"
  1275. data-config-namespace="ng"
  1276. ></textarea>
  1277. </div>
  1278. </div>
  1279. <label class="setting-label wwwOnly wwwZenzaOnly setting-ng-label">
  1280. <input
  1281. type="checkbox"
  1282. class="setting-form"
  1283. data-config-name="syncZenza"
  1284. data-config-namespace="ng"
  1285. >
  1286. <span>NGタグ・投稿者をZenzaWatchにも反映する</span>
  1287. </label>
  1288.  
  1289. <h2 class="setting-fav">強調表示設定</h2>
  1290. <div class="setting-fav-textarea setting-fav">
  1291. <div class="setting-fav-text-column">
  1292. 投稿者ID
  1293. <textarea
  1294. class="setting-form"
  1295. data-config-name="owner"
  1296. data-config-namespace="fav"
  1297. ></textarea>
  1298. </div>
  1299. <div class="setting-fav-text-column">
  1300. タグ
  1301. <textarea
  1302. class="setting-form"
  1303. data-config-name="tag"
  1304. data-config-namespace="fav"
  1305. ></textarea>
  1306. </div>
  1307. <div class="setting-fav-text-column">
  1308. タイトル・説明文
  1309. <textarea
  1310. class="setting-form"
  1311. data-config-name="word"
  1312. data-config-namespace="fav"
  1313. ></textarea>
  1314. </div>
  1315. </div>
  1316.  
  1317. </div>
  1318.  
  1319. <div class="footer-menu">
  1320. <div class="close-setting-menu">
  1321. <button
  1322. class="pocket-button command"
  1323. data-command="toggle-setting"
  1324. >戻 る</button>
  1325. </div>
  1326. </div>
  1327.  
  1328. </div>
  1329. </div>
  1330. </template>
  1331. `).trim();
  1332.  
  1333. const __ng_css__ = (`
  1334. /* .item_cell 将棋盤ランキング .item 従来のランキングと検索 */
  1335.  
  1336. .item_cell.is-ng-wait .item,
  1337. .item.is-ng-wait {
  1338. outline: 1px dotted rgba(192, 192, 192, 0.8);
  1339. }
  1340.  
  1341. .item_cell.is-ng-queue .item,
  1342. .item.is-ng-queue {
  1343. outline: 2px dotted rgba(192, 192, 192, 0.8);
  1344. }
  1345.  
  1346. .item_cell.is-ng-current .item,
  1347. .item.is-ng-current {
  1348. outline: 3px dotted rgba(128, 225, 128, 0.8);
  1349. }
  1350.  
  1351. .item_cell.is-ng-resolved .item,
  1352. .item.is-ng-resolved {
  1353. outline: 0px solid green;
  1354. }
  1355.  
  1356. .item_cell.is-fav-favorited .item,
  1357. .item.is-fav-favorited {
  1358. outline: 3px dotted orange;
  1359. outline-offset: 3px;
  1360. }
  1361. .item.videoRanking.is-fav-favorited {
  1362. outline-offset: -3px;
  1363. }
  1364.  
  1365. .item_cell.is-ng-rejected {
  1366. opacity: 0;
  1367. pointer-events: none;
  1368. }
  1369.  
  1370. .item.is-ng-rejected {
  1371. display: none;
  1372. opacity: 0;
  1373. pointer-events: none;
  1374. }
  1375. body.is-ng-disable .is-ng-rejected {
  1376. outline: none;
  1377. display: block !important;
  1378. pointer-events: auto;
  1379. opacity: 0.5;
  1380. }
  1381.  
  1382. .is-ng-failed {
  1383. outline: black dotted 1px;
  1384. opacity: 0.5;
  1385. }
  1386.  
  1387. /* チャンネル検索 */
  1388. #search .item.is-ng-rejected {
  1389. display: none;
  1390. }
  1391.  
  1392. /* 新検索β */
  1393. #row-results .is-ng-wait {
  1394. outline: 1px dotted rgba(192, 192, 192, 0.8);
  1395. }
  1396.  
  1397. #row-results .is-ng-queue {
  1398. outline: 2px dotted rgba(192, 192, 192, 0.8);
  1399. }
  1400.  
  1401. #row-results .is-ng-rejected {
  1402. display: none;
  1403. }
  1404.  
  1405. #row-results .is-fav-favorited {
  1406. outline: 4px dotted pink;
  1407. }
  1408.  
  1409.  
  1410. `).trim();
  1411.  
  1412. // TODO: ライブラリ化
  1413. const util = MylistPocket.util = (() => {
  1414. const util = {};
  1415.  
  1416. util.addStyle = function(styles, id) {
  1417. var elm = document.createElement('style');
  1418. elm.type = 'text/css';
  1419. if (id) { elm.id = id; }
  1420.  
  1421. var text = styles.toString();
  1422. text = document.createTextNode(text);
  1423. elm.appendChild(text);
  1424. var head = document.getElementsByTagName('head');
  1425. head = head[0];
  1426. head.appendChild(elm);
  1427. return elm;
  1428. };
  1429.  
  1430. util.mixin = function(self, o) {
  1431. _.each(Object.keys(o), f => {
  1432. if (!_.isFunction(o[f])) { return; }
  1433. if (_.isFunction(self[f])) { return; }
  1434. self[f] = o[f].bind(o);
  1435. });
  1436. };
  1437.  
  1438. util.createWebWorker = function(func) {
  1439. const src = func.toString().replace(/^function.*?\{/, '').replace(/}$/, '');
  1440. const blob = new Blob([src], {type: 'text\/javascript'});
  1441. const url = URL.createObjectURL(blob);
  1442.  
  1443. return new Worker(url);
  1444. };
  1445.  
  1446. util.attachShadowDom = function({host, tpl, mode = 'open'}) {
  1447. const root = host.attachShadow ?
  1448. host.attachShadow({mode}) : host.createShadowRoot();
  1449. const node = document.importNode(tpl.content, true);
  1450. root.appendChild(node);
  1451. return root;
  1452. };
  1453.  
  1454.  
  1455.  
  1456. util.getWatchId = function(url) {
  1457. /\/?watch\/([a-z0-9]+)/.test(url || location.pathname);
  1458. return RegExp.$1;
  1459. };
  1460.  
  1461. util.isLogin = function() {
  1462. return document.getElementsByClassName('siteHeaderLogin').length < 1;
  1463. };
  1464.  
  1465. util.escapeHtml = function(text) {
  1466. var map = {
  1467. '&': '&amp;',
  1468. '\x27': '&#39;',
  1469. '"': '&quot;',
  1470. '<': '&lt;',
  1471. '>': '&gt;'
  1472. };
  1473. return text.replace(/[&"'<>]/g, char => {
  1474. return map[char];
  1475. });
  1476. };
  1477.  
  1478. util.unescapeHtml = function(text) {
  1479. var map = {
  1480. '&amp;' : '&' ,
  1481. '&#39;' : '\x27',
  1482. '&quot;' : '"',
  1483. '&lt;' : '<',
  1484. '&gt;' : '>'
  1485. };
  1486. return text.replace(/(&amp;|&#39;|&quot;|&lt;|&gt;)/g, char => {
  1487. return map[char];
  1488. });
  1489. };
  1490.  
  1491. util.escapeRegs = function(text) {
  1492. const map = {
  1493. '\\': '\\\\',
  1494. '*': '\\*',
  1495. '+': '\\+',
  1496. '.': '\\.',
  1497. '?': '\\?',
  1498. '{': '\\{',
  1499. '}': '\\}',
  1500. '(': '\\(',
  1501. ')': '\\)',
  1502. '[': '\\[',
  1503. ']': '\\]',
  1504. '^': '\\^',
  1505. '$': '\\$',
  1506. '-': '\\-',
  1507. '|': '\\|',
  1508. '/': '\\/',
  1509. };
  1510. return text.replace(/[\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/]/g, char => {
  1511. return map[char];
  1512. });
  1513. };
  1514.  
  1515. util.hasLargeThumbnail = function(videoId) { // return true;
  1516. // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳
  1517. // ※この数字以降でもごく稀に例外はある。
  1518. var threthold = 16371888;
  1519. var cid = videoId.substr(0, 2);
  1520. if (cid !== 'sm') { return false; }
  1521.  
  1522. var fid = videoId.substr(2) * 1;
  1523. if (fid < threthold) { return false; }
  1524.  
  1525. return true;
  1526. };
  1527.  
  1528. util.httpLink = function(html) {
  1529. let links = {}, keyCount = 0;
  1530. const getTmpKey = function() { return ` <!--${keyCount++}--> `; };
  1531.  
  1532.  
  1533. html = html.replace(/@([a-zA-Z0-9_]+)/g,
  1534. (g, id) => {
  1535. const tmpKey = getTmpKey();
  1536. links[tmpKey] =
  1537. ` <a href="https://twitter.com/${id}" class="twitterLink" rel="noopener" target="_blank">@${id}</a> `;
  1538. return tmpKey;
  1539. });
  1540.  
  1541.  
  1542. html = html.replace(/(im)(\d+)/g,
  1543. ` <a href="//seiga.nicovideo.jp/seiga/$1$2" class="seigaLink" rel="noopener" target="_blank">$1$2</a> `);
  1544. html = html.replace(/(co)(\d+)/g,
  1545. ` <a href="//com.nicovideo.jp/community/$1$2" class="communityLink" rel="noopener" target="_blank">$1$2</a> `);
  1546. html = html.replace(/(watch|mylist|user)\/(\d+)/g, ` <a href="/$1/$2" rel="noopener" class="videoLink target-change">$1/$2</a> `);
  1547. html = html.replace(/(sm|nm|so)(\d+)/g, ` <a href="/watch/$1$2" rel="noopener" class="videoLink target-change">$1$2</a> `);
  1548.  
  1549. let linkmatch = /<a.*?<\/a>/, n;
  1550. html = html.split('<br />').join(' <br /> ');
  1551. while ((n = linkmatch.exec(html)) !== null) {
  1552. let tmpKey = getTmpKey();
  1553. links[tmpKey] = n;
  1554. html = html.replace(n, tmpKey);
  1555. }
  1556.  
  1557. html = html.replace(/\((https?:\/\/[\x21-\x3b\x3d-\x7e]+)\)/gi, '( $1 )');
  1558. html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)http/gi, '$1 http');
  1559. html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)/gi, '<a href="$1" rel="noopener" target="_blank" class="otherSite">$1</a>');
  1560. Object.keys(links).forEach(tmpKey => {
  1561. html = html.replace(tmpKey, links[tmpKey]);
  1562. });
  1563.  
  1564. html = html.split(' <br /> ').join('<br />');
  1565. return html;
  1566. };
  1567.  
  1568. util.getSleepPromise = function(sleepTime, label = 'sleep') {
  1569. return function(result) {
  1570. return new Promise(resolve => {
  1571. //console.time('sleep promise...' + label);
  1572. window.setTimeout(() => {
  1573. //console.timeEnd('sleep promise...' + label);
  1574. return resolve(result);
  1575. }, sleepTime);
  1576. });
  1577. };
  1578. };
  1579.  
  1580. util.getPageLanguage = function() {
  1581. try {
  1582. var h = document.getElementsByClassName('html')[0];
  1583. return h.lang || 'ja-JP';
  1584. } catch(e) {
  1585. return 'ja-JP';
  1586. }
  1587. };
  1588.  
  1589.  
  1590. const videoIdReg = /^[a-z]{2}\d+$/;
  1591. util.getThumbnailUrlByVideoId = function(videoId) {
  1592. if (!videoIdReg.test(videoId)) {
  1593. return null;
  1594. }
  1595. const fileId = parseInt(videoId.substr(2), 10);
  1596. const num = (fileId % 4) + 1;
  1597. const large = util.hasLargeThumbnail(videoId) ? '.L' : '';
  1598. return '//tn-skr' + num + '.smilevideo.jp/smile?i=' + fileId + large;
  1599. };
  1600.  
  1601. util.isFirefox = function() {
  1602. return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
  1603. };
  1604.  
  1605. return util;
  1606. })();
  1607.  
  1608.  
  1609. class Emitter {
  1610. constructor() {
  1611. }
  1612.  
  1613. on(name, callback) {
  1614. if (!this._events) { this._events = {}; }
  1615. name = name.toLowerCase();
  1616. if (!this._events[name]) {
  1617. this._events[name] = [];
  1618. }
  1619. this._events[name].push(callback);
  1620. }
  1621.  
  1622. clear(name) {
  1623. if (!this._events) { this._events = {}; }
  1624. if (name) {
  1625. this._events[name] = [];
  1626. } else {
  1627. this._events = {};
  1628. }
  1629. }
  1630.  
  1631. emit(name) {
  1632. if (!this._events) { this._events = {}; }
  1633. name = name.toLowerCase();
  1634. if (!this._events.hasOwnProperty(name)) { return; }
  1635. const e = this._events[name];
  1636. const arg = Array.prototype.slice.call(arguments, 1);
  1637. for (let i =0, len = e.length; i < len; i++) {
  1638. e[i].apply(null, arg);
  1639. }
  1640. }
  1641.  
  1642. emitAsync(...args) {
  1643. window.setTimeout(() => {
  1644. this.emit(...args);
  1645. }, 0);
  1646. }
  1647. }
  1648.  
  1649. MylistPocket.emitter = util.emitter = new Emitter();
  1650.  
  1651. const ZenzaDetector = (function() {
  1652. let isReady = false;
  1653. let Zenza = null;
  1654. const emitter = new Emitter();
  1655.  
  1656. const initialize = function() {
  1657. const onZenzaReady = () => {
  1658. isReady = true;
  1659. Zenza = window.ZenzaWatch;
  1660.  
  1661. Zenza.emitter.on('hideHover', () => {
  1662. util.emitter.emit('hideHover');
  1663. });
  1664.  
  1665. Zenza.emitter.on('csrfToken', (token) => {
  1666. util.emitter.emit('csrfToken', token);
  1667. });
  1668.  
  1669. let popup = document.getElementById('mylistPocket-popup');
  1670. let defaultContainer = document.getElementById('mylistPocketDomContainer');
  1671. let zenzaContainer;
  1672. Zenza.emitter.on('fullScreenStatusChange', isFull => {
  1673. if (isFull) {
  1674. if (!zenzaContainer) {
  1675. zenzaContainer = document.querySelector('.zenzaPlayerContainer');
  1676. }
  1677. zenzaContainer.appendChild(popup);
  1678. } else {
  1679. defaultContainer.appendChild(popup);
  1680. }
  1681. });
  1682. emitter.emit('ready', Zenza);
  1683. };
  1684.  
  1685. if (window.ZenzaWatch && window.ZenzaWatch.ready) {
  1686. window.console.log('ZenzaWatch is Ready');
  1687. onZenzaReady();
  1688. } else {
  1689. document.body.addEventListener('ZenzaWatchInitialize', function() {
  1690. window.console.log('ZenzaWatchInitialize');
  1691. onZenzaReady();
  1692. });
  1693. }
  1694. };
  1695.  
  1696. const detect = function() {
  1697. return new Promise(res => {
  1698. if (isReady) {
  1699. return res(Zenza);
  1700. }
  1701. emitter.on('ready', () => {
  1702. res(Zenza);
  1703. });
  1704. });
  1705. };
  1706.  
  1707. return {
  1708. initialize: initialize,
  1709. detect: detect
  1710. };
  1711.  
  1712. })();
  1713.  
  1714. const StorageWriter = (function() {
  1715. const emitter = new Emitter();
  1716. // マイページのJSON.stringifyがPrototype.jsのせいでぶっこわれているので
  1717. // 汚染されていないWebWorkerを使って書き込む
  1718. const func = function(self) {
  1719. self.onmessage = function(e) {
  1720. const key = e.data.key;
  1721. const value = e.data.value;
  1722. const storage = e.data.storage;
  1723. self.postMessage({key, value: JSON.stringify(value), storage});
  1724. };
  1725. };
  1726.  
  1727. const worker = util.createWebWorker(func);
  1728. worker.addEventListener('message', (e) => {
  1729. const key = e.data.key;
  1730. const value = e.data.value;
  1731. const storage = e.data.storage === 'session' ? sessionStorage : localStorage;
  1732. storage[key] = value;
  1733. emitter.emit('write', {key, value, storage: e.data.storage});
  1734. });
  1735.  
  1736. return {
  1737. write: function({key, value, storage = 'local'}) {
  1738. worker.postMessage({
  1739. key,
  1740. value,
  1741. storage
  1742. });
  1743. },
  1744. on: function(...args) { emitter.on(...args); }
  1745. };
  1746. })();
  1747.  
  1748. MylistPocket.debug.writer = StorageWriter;
  1749.  
  1750.  
  1751. const config = (function() {
  1752. const prefix = PRODUCT + '_config_';
  1753. const emitter = new Emitter();
  1754.  
  1755. const defaultConfig = {
  1756. debug: false,
  1757.  
  1758. 'videoInfo.openNewWindow': false,
  1759.  
  1760. 'ng.enable': false,
  1761. 'ng.owner': '',
  1762. 'ng.word': '',
  1763. 'ng.tag': '',
  1764. 'ng.syncZenza': false,
  1765.  
  1766. 'fav.owner': '',
  1767. 'fav.word': '',
  1768. 'fav.tag': ''
  1769. };
  1770.  
  1771. const config = {};
  1772. let noEmit = false;
  1773.  
  1774.  
  1775. emitter.refresh = (emitChange = false) => {
  1776. //console.log('keys', Object.keys(localStorage));
  1777. Object.keys(defaultConfig).forEach(key => {
  1778. var storageKey = prefix + key;
  1779. //console.log('conf', storageKey, localStorage.hasOwnProperty(storageKey), localStorage[storageKey]);
  1780. if (localStorage.hasOwnProperty(storageKey) || localStorage[storageKey] !== undefined) {
  1781. try {
  1782. let lastValue = config[key];
  1783. let newValue = JSON.parse(localStorage.getItem(storageKey));
  1784. if (lastValue !== newValue) {
  1785. config[key] = newValue;
  1786. if (emitChange) {
  1787. emitter.emit('key', newValue);
  1788. emitter.emit('@update', {key, value: newValue});
  1789. }
  1790. }
  1791. } catch (e) {
  1792. window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e);
  1793. config[key] = defaultConfig[key];
  1794. }
  1795. } else {
  1796. config[key] = defaultConfig[key];
  1797. }
  1798. });
  1799. };
  1800. emitter.refresh();
  1801.  
  1802. //Chrome Canary v60でlocalStorageにデータが有ってもhasOwnPropertyがfalseを返したりする
  1803. //function dump() {
  1804. // Object.keys(localStorage).forEach(key => {
  1805. // console.log('DUMP key="%s" %s', key, localStorage.hasOwnProperty(key), localStorage.getItem(key));
  1806. // });
  1807. //}
  1808. //dump();
  1809. //window.setTimeout(dump, 3000);
  1810.  
  1811. emitter.getValue = function(key, refresh) {
  1812. if (refresh) {
  1813. emitter.refreshValue(key);
  1814. }
  1815. return config[key];
  1816. };
  1817.  
  1818. emitter.setValue = function(key, value) {
  1819. if (config[key] !== value && arguments.length >= 2) {
  1820. var storageKey = prefix + key;
  1821. StorageWriter.write({key: storageKey, value});
  1822. config[key] = value;
  1823. emitter.emit(key, value);
  1824. emitter.emit('@update', {key, value});
  1825. //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value);
  1826. }
  1827. };
  1828.  
  1829. emitter.clearConfig = function() {
  1830. noEmit = true;
  1831. Object.keys(defaultConfig).forEach(key => {
  1832. if (_.contains(['message', 'debug'], key)) { return; }
  1833. var storageKey = prefix + key;
  1834. try {
  1835. if (localStorage.hasOwnProperty(storageKey) || localStorage[storageKey] !== undefined) {
  1836. localStorage.removeItem(storageKey);
  1837. }
  1838. config[key] = defaultConfig[key];
  1839. } catch (e) {}
  1840. });
  1841. noEmit = false;
  1842. };
  1843.  
  1844. emitter.getKeys = function() {
  1845. return Object.keys(defaultConfig);
  1846. };
  1847.  
  1848. emitter.namespace = function(name) {
  1849. return {
  1850. getValue: (key) => { return emitter.getValue(name + '.' + key); },
  1851. setValue: (key, value) => { emitter.setValue(name + '.' + key, value); },
  1852. refresh: () => { emitter.refresh(); },
  1853. on: (key, func) => {
  1854. if (key === '@update') {
  1855. emitter.on('@update', ({key, value}) => {
  1856. const pre = name + '.';
  1857. //console.log('@update', key, value, pre);
  1858. if (key.startsWith(pre)) {
  1859. func({key: key.replace(pre, ''), value});
  1860. }
  1861. });
  1862. } else {
  1863. emitter.on(name + '.' + key, func);
  1864. }
  1865. }
  1866. };
  1867. };
  1868.  
  1869. StorageWriter.on('write', ({key, value, storage}) => {
  1870. if (storage === 'session') { return; }
  1871. const _key = key.replace(prefix, '');
  1872. if (!config.hasOwnProperty(_key)) { return; }
  1873. MylistPocket.broadcast.postMessage(
  1874. {type: 'config-update', key, value, storage}
  1875. );
  1876. });
  1877.  
  1878. return emitter;
  1879. })();
  1880. MylistPocket.config = config;
  1881.  
  1882. MylistPocket.broadcast = (function(config) {
  1883. if (!window.BroadcastChannel) { return; }
  1884. const broadcastChannel = new window.BroadcastChannel(PRODUCT);
  1885.  
  1886. const onBroadcastMessage = (e) => {
  1887. const data = e.data;
  1888. //window.console.info(`${PRODUCT}: onBroadcastMessage`, data);
  1889. switch (data.type) {
  1890. case 'config-update':
  1891. config.refresh(true);
  1892. break;
  1893. }
  1894. };
  1895.  
  1896. broadcastChannel.addEventListener('message', onBroadcastMessage);
  1897.  
  1898. return {
  1899. postMessage: (...args) => { broadcastChannel.postMessage(...args); }
  1900. };
  1901. })(config);
  1902.  
  1903.  
  1904.  
  1905. const CacheStorage = (function() {
  1906. var PREFIX = PRODUCT + '_cache_';
  1907.  
  1908. class CacheStorage {
  1909.  
  1910. constructor(storage, gc = false) {
  1911. this._storage = storage;
  1912. this._memory = {};
  1913. if (gc) { this.gc(); }
  1914. Object.keys(storage).forEach((key) => {
  1915. if (key.indexOf(PREFIX) === 0) {
  1916. this._memory[key] = storage[key];
  1917. }
  1918. });
  1919. }
  1920.  
  1921. gc() {
  1922. const storage = this._storage;
  1923. Object.keys(storage).forEach((key) => {
  1924. if (key.indexOf(PREFIX) === 0) {
  1925. let item;
  1926. try {
  1927. item = JSON.parse(this._storage[key]);
  1928. } catch(e) {
  1929. storage.removeItem(key);
  1930. }
  1931. //console.info(
  1932. // `key: ${key}, expiredAt: ${item.expiredAt}, now: ${Date.now()}`);
  1933. if (item.expiredAt === '' || item.expiredAt > Date.now()) {
  1934. //console.info('not expired: ', key);
  1935. return;
  1936. }
  1937. //console.info('cache expired: ', key, item.expiredAt);
  1938. storage.removeItem(key);
  1939. }
  1940. });
  1941. }
  1942.  
  1943. setItem(key, data, expireTime) {
  1944. key = PREFIX + key;
  1945. const expiredAt =
  1946. typeof expireTime === 'number' ? (Date.now() + expireTime) : '';
  1947.  
  1948. const cacheData = {
  1949. data: data,
  1950. type: typeof data,
  1951. expiredAt: expiredAt
  1952. };
  1953.  
  1954. this._memory[key] = cacheData;
  1955. StorageWriter.write({
  1956. key,
  1957. value: cacheData,
  1958. storage: this._storage === sessionStorage ? 'session' : 'local'
  1959. });
  1960. //this._storage[key] = JSON.stringify(cacheData);
  1961. }
  1962.  
  1963. getItem(key) {
  1964. key = PREFIX + key;
  1965. if (!(this._storage.hasOwnProperty(key) || this._storage[key] !== undefined)) {
  1966. return null;
  1967. }
  1968. let item = null;
  1969. try {
  1970. item = JSON.parse(this._storage[key]);
  1971. } catch(e) {
  1972. delete this._memory[key];
  1973. this._storage.removeItem(key);
  1974. return null;
  1975. }
  1976.  
  1977. if (item.expiredAt === '' || item.expiredAt > Date.now()) {
  1978. return item.data;
  1979. }
  1980. return null;
  1981. }
  1982.  
  1983. removeItem(key) {
  1984. if (this._memory.hasOwnProperty(key)) {
  1985. delete this._memory[key];
  1986. }
  1987. key = PREFIX + key;
  1988. if (this._storage.hasOwnProperty(key) || this._storage[key] !== undefined) {
  1989. this._storage.removeItem(key);
  1990. }
  1991. }
  1992.  
  1993. clear() {
  1994. const storage = this._storage;
  1995. this._memory = {};
  1996. Object.keys(storage).forEach((v) => {
  1997. if (v.indexOf(PREFIX) === 0) {
  1998. storage.removeItem(v);
  1999. }
  2000. });
  2001. }
  2002. }
  2003. return CacheStorage;
  2004. })();
  2005. MylistPocket.debug.sessionCache = new CacheStorage(sessionStorage, true);
  2006. MylistPocket.debug.localCache = new CacheStorage(localStorage, true);
  2007.  
  2008. const WindowMessageEmitter = (function() {
  2009. const emitter = new Emitter();
  2010. const knownSource = [];
  2011.  
  2012. const onMessage = (event) => {
  2013. if (_.indexOf(knownSource, event.source) < 0 //&&
  2014. //event.origin !== location.protocol + '//ext.nicovideo.jp'
  2015. ) { return; }
  2016.  
  2017. try {
  2018. var data = JSON.parse(event.data);
  2019. if (data.id !== PRODUCT) { return; }
  2020.  
  2021. emitter.emit('onMessage', data.body, data.type);
  2022. } catch (e) {
  2023. console.log(
  2024. '%cMylistPocket.Error: window.onMessage - ',
  2025. 'color: red; background: yellow',
  2026. e,
  2027. event
  2028. );
  2029. console.log('%corigin: ', 'background: yellow;', event.origin);
  2030. console.log('%cdata: ', 'background: yellow;', event.data);
  2031. console.trace();
  2032. }
  2033. };
  2034.  
  2035. emitter.addKnownSource = (win) => {
  2036. knownSource.push(win);
  2037. };
  2038.  
  2039. window.addEventListener('message', onMessage);
  2040.  
  2041. return emitter;
  2042. })();
  2043.  
  2044.  
  2045. const CrossDomainGate = (function() {
  2046.  
  2047. class CrossDomainGate extends Emitter {
  2048. constructor(params) {
  2049. super();
  2050.  
  2051. this._baseUrl = params.baseUrl;
  2052. this._origin = params.origin || location.href;
  2053. this._type = params.type;
  2054. this._messager = params.messager || WindowMessageEmitter;
  2055.  
  2056. this._loaderFrame = null;
  2057. this._sessions = {};
  2058. this._initializeStatus = '';
  2059. }
  2060.  
  2061. _initializeFrame() {
  2062. switch (this._initializeStatus) {
  2063. case 'done':
  2064. return new Promise((resolve) => {
  2065. window.setTimeout(() => { resolve(); }, 0);
  2066. });
  2067. case 'initializing':
  2068. return new Promise((resolve, reject) => {
  2069. this.on('initialize', (e) => {
  2070. if (e.status === 'ok') { resolve(); } else { reject(e); }
  2071. });
  2072. });
  2073. case '':
  2074. this._initializeStatus = 'initializing';
  2075. var initialPromise = new Promise((resolve, reject) => {
  2076. this._sessions.initial = {
  2077. promise: initialPromise,
  2078. resolve: resolve,
  2079. reject: reject
  2080. };
  2081.  
  2082. setTimeout(() => {
  2083. if (this._initializeStatus !== 'done') {
  2084. var rej = {
  2085. status: 'fail',
  2086. message: 'CrossDomainGate初期化タイムアウト (' + this._type + ')'
  2087. };
  2088. reject(rej);
  2089. this.emit('initialize', rej);
  2090. }
  2091. }, 60 * 1000);
  2092. this._initializeCrossDomainGate();
  2093. });
  2094. return initialPromise;
  2095. }
  2096. }
  2097.  
  2098. _initializeCrossDomainGate() {
  2099. this._initializeCrossDomainGate = _.noop;
  2100. this._messager.on('onMessage', this._onMessage.bind(this));
  2101.  
  2102. console.log('%c initialize ' + this._type, 'background: lightgreen;');
  2103.  
  2104. const loaderFrame = document.createElement('iframe');
  2105.  
  2106. loaderFrame.name = this._type + 'Loader';
  2107. loaderFrame.className = 'xDomainLoaderFrame ' + this._type;
  2108. document.body.appendChild(loaderFrame);
  2109.  
  2110. this._loaderFrame = loaderFrame;
  2111. this._loaderWindow = loaderFrame.contentWindow;
  2112. this._messager.addKnownSource(this._loaderWindow);
  2113. this._loaderWindow.location.href = this._baseUrl + '#' + TOKEN;
  2114. }
  2115.  
  2116. _onMessage(data, type) {
  2117. if (type !== this._type) {
  2118. return;
  2119. }
  2120. const info = data.message;
  2121. const token = info.token;
  2122. const sessionId = info.sessionId;
  2123. const status = info.status;
  2124. const command = info.command || 'loadUrl';
  2125. let session = this._sessions[sessionId];
  2126.  
  2127. if (status === 'initialized') {
  2128. this._initializeStatus = 'done';
  2129. this._sessions.initial.resolve();
  2130. this.emitAsync('initialize', {status: 'ok'});
  2131. return;
  2132. }
  2133.  
  2134. if (token !== TOKEN) {
  2135. window.console.log('invalid token:', token, TOKEN);
  2136. return;
  2137. }
  2138.  
  2139. switch (command) {
  2140. case 'dumpConfig':
  2141. this._onDumpConfig(info.body);
  2142. break;
  2143.  
  2144. default:
  2145. if (!session) { return; }
  2146. if (status === 'ok') { session.resolve(info.body); }
  2147. else { session.reject({ message: status }); }
  2148. session = null;
  2149. delete this._sessions[sessionId];
  2150. break;
  2151. }
  2152. }
  2153.  
  2154. load(url, options) {
  2155. return this._postMessage({
  2156. command: 'loadUrl',
  2157. url: url,
  2158. options: options
  2159. }, true);
  2160. }
  2161.  
  2162. _postMessage(message, needPromise) {
  2163. return new Promise((resolve, reject) => {
  2164. message.sessionId = this._type + '_' + Math.random();
  2165. message.token = TOKEN;
  2166. if (needPromise) {
  2167. this._sessions[message.sessionId] = {
  2168. resolve: resolve,
  2169. reject: reject
  2170. };
  2171. }
  2172.  
  2173. return this._initializeFrame().then(() => {
  2174. try {
  2175. this._loaderWindow.postMessage(
  2176. JSON.stringify(message),
  2177. this._origin
  2178. );
  2179. } catch (e) {
  2180. console.log('%cException!', 'background: red;', e);
  2181. }
  2182. });
  2183. });
  2184. }
  2185.  
  2186.  
  2187. }
  2188.  
  2189. return CrossDomainGate;
  2190. })();
  2191.  
  2192.  
  2193.  
  2194. const CsrfTokenLoader = (() => {
  2195. const cacheStorage = new CacheStorage(
  2196. location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage);
  2197. const TIMEOUT = 10 * 1000;
  2198. const CACHE_EXPIRE_TIME = 60 * 30 * 1000;
  2199.  
  2200. class CsrfTokenLoader {
  2201. static load() {
  2202. return new Promise((resolve, reject) => {
  2203. const cache = cacheStorage.getItem('csrfToken');
  2204. if (cacheStorage.getItem('csrfToken')) {
  2205. return resolve(cache);
  2206. }
  2207.  
  2208. let timeoutTimer = window.setTimeout(() => {
  2209. reject('timeout');
  2210. }, TIMEOUT);
  2211.  
  2212. return CsrfTokenLoader._getToken().then((token) => {
  2213. window.clearTimeout(timeoutTimer);
  2214. CsrfTokenLoader.saveToCache(token);
  2215. resolve(token);
  2216. });
  2217. });
  2218. }
  2219.  
  2220. static saveToCache(token) {
  2221. cacheStorage.setItem('csrfToken', token, CACHE_EXPIRE_TIME);
  2222. }
  2223.  
  2224. static _getToken() {
  2225. const url = 'http://www.nicovideo.jp/mylist_add/video/sm9';
  2226. const tokenReg = /NicoAPI\.token *= *["']([a-z0-9\-]+)["'];/;
  2227.  
  2228. return fetch(url, {
  2229. credentials: 'include'
  2230. }).then((res) => {
  2231. return res.text();
  2232. }).then((result) => {
  2233. if (tokenReg.test(result)) {
  2234. let token = RegExp.$1;
  2235. return Promise.resolve(token);
  2236. } else {
  2237. return Promise.reject('token parse error');
  2238. }
  2239. });
  2240. }
  2241. }
  2242.  
  2243. util.emitter.on('csrfToken', (token) => {
  2244. CsrfTokenLoader.saveToCache(token);
  2245. });
  2246.  
  2247. return CsrfTokenLoader;
  2248. })();
  2249.  
  2250. MylistPocket.debug.CsrfTokenLoader = CsrfTokenLoader;
  2251.  
  2252. const ThumbInfoLoader = (() => {
  2253. const BASE_URL = location.protocol + '//ext.nicovideo.jp/';
  2254. const MESSAGE_ORIGIN = location.protocol + '//ext.nicovideo.jp/';
  2255. const CACHE_EXPIRE_TIME = 3 * 60 * 60 * 1000;
  2256. //const CACHE_EXPIRE_TIME = 60 * 1000;
  2257. let gate = null;
  2258. let cacheStorage = new CacheStorage(localStorage);
  2259.  
  2260. class ThumbInfoLoader {
  2261.  
  2262. constructor() {
  2263. this._emitter = new Emitter();
  2264.  
  2265. gate = new CrossDomainGate({
  2266. baseUrl: BASE_URL,
  2267. origin: MESSAGE_ORIGIN,
  2268. type: 'thumbInfo' + PRODUCT,
  2269. messager: WindowMessageEmitter
  2270. });
  2271. }
  2272.  
  2273. _onMessage(data, type) {
  2274. if (type !== 'videoInfoLoader') { return; }
  2275. const info = data.message;
  2276.  
  2277. this.emit('load', info, 'THUMB_WATCH');
  2278. }
  2279.  
  2280. _parseXml(xmlText) {
  2281. const parser = new DOMParser();
  2282. const xml = parser.parseFromString(xmlText, 'text/xml');
  2283. const val = (name) => {
  2284. var elms = xml.getElementsByTagName(name);
  2285. if (elms.length < 1) {
  2286. return null;
  2287. }
  2288. return elms[0].innerHTML;
  2289. };
  2290.  
  2291. const resp = xml.getElementsByTagName('nicovideo_thumb_response');
  2292. if (resp.length < 1 || resp[0].getAttribute('status') !== 'ok') {
  2293. return {
  2294. status: 'fail',
  2295. code: val('code'),
  2296. message: val('description')
  2297. };
  2298. }
  2299.  
  2300. const duration = (() => {
  2301. const tmp = val('length').split(':');
  2302. return parseInt(tmp[0], 10) * 60 + parseInt(tmp[1], 10);
  2303. })();
  2304. const watchId = val('watch_url').split('/').reverse()[0];
  2305. const postedAt = (new Date(val('first_retrieve'))).toLocaleString();
  2306. const tags = (() => {
  2307. const result = [], t = xml.getElementsByTagName('tag');
  2308. _.each(t, (tag) => {
  2309. result.push(tag.innerHTML);
  2310. });
  2311. return result;
  2312. })();
  2313.  
  2314. const result = {
  2315. status: 'ok',
  2316. _format: 'thumbInfo',
  2317. v: watchId,
  2318. id: val('video_id'),
  2319. title: val('title'),
  2320. description: val('description'),
  2321. thumbnail: val('thumbnail_url'),
  2322. movieType: val('movie_type'),
  2323. lastResBody: val('last_res_body'),
  2324. duration: duration,
  2325. postedAt: postedAt,
  2326. mylistCount: parseInt(val('mylist_counter'), 10),
  2327. viewCount: parseInt(val('view_counter'), 10),
  2328. commentCount: parseInt(val('comment_num'), 10),
  2329. tagList: tags
  2330. };
  2331. const userId = val('user_id');
  2332. if (userId !== null) {
  2333. result.owner = {
  2334. type: 'user',
  2335. id: userId,
  2336. name: val('user_nickname') || '(非公開ユーザー)',
  2337. url: userId ? ('//www.nicovideo.jp/user/' + userId) : '#',
  2338. icon: val('user_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg'
  2339. };
  2340. }
  2341. const channelId = val('ch_id');
  2342. if (channelId !== null) {
  2343. result.owner = {
  2344. type: 'channel',
  2345. id: channelId,
  2346. name: val('ch_name') || '(非公開ユーザー)',
  2347. url: '//ch.nicovideo.jp/ch' + channelId,
  2348. icon: val('ch_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg'
  2349. };
  2350. }
  2351.  
  2352. return result;
  2353. }
  2354.  
  2355. loadXml(watchId) {
  2356. return this.load(watchId, 'xml');
  2357. }
  2358.  
  2359. load(watchId, format) {
  2360. return new Promise((resolve, reject) => {
  2361. const cache = cacheStorage.getItem('thumbInfo_' + watchId);
  2362.  
  2363. const onLoad = (xml) => {
  2364. const result = this._parseXml(xml);
  2365. result.fromCache = !!cache;
  2366. if (result.status === 'ok') {
  2367. if (!cache) {
  2368. cacheStorage.setItem('thumbInfo_' + watchId, xml, CACHE_EXPIRE_TIME);
  2369. }
  2370. resolve({data: format === 'xml' ? xml : result, watchId});
  2371. } else {
  2372. reject({data: format === 'xml' ? xml : result, watchId});
  2373. }
  2374. };
  2375.  
  2376. if (cache) {
  2377. //console.log('cache exist: ', watchId);
  2378. onLoad(cache);
  2379. //window.setTimeout(() => { onLoad(cache); }, 0);
  2380. return;
  2381. }
  2382.  
  2383. gate.load(BASE_URL + 'api/getthumbinfo/' + watchId).then(onLoad);
  2384. });
  2385. }
  2386. }
  2387.  
  2388. const loader = new ThumbInfoLoader();
  2389. return {
  2390. load: (watchId) => { return loader.load(watchId); },
  2391. loadXml: (watchId) => { return loader.loadXml(watchId); },
  2392. loadOwnerInfo: (watchId) => {
  2393. return loader.load(watchId).then((info) => {
  2394. const owner = info.data.owner;
  2395. if (!owner) {
  2396. return {};
  2397. }
  2398.  
  2399. const lang = util.getPageLanguage();
  2400. const prefix = owner.type === 'user' ? '投稿者: ' : '提供: ';
  2401. const suffix =
  2402. (owner.type === 'user' && lang === 'ja-JP') ? ' さん' : '';
  2403. owner.localeName = `${prefix}${owner.name}${suffix}`;
  2404. return owner;
  2405. });
  2406. }
  2407. };
  2408.  
  2409. })();
  2410.  
  2411. MylistPocket.debug.ThumbInfoLoader = ThumbInfoLoader;
  2412.  
  2413.  
  2414.  
  2415. const DeflistApiLoader = ((CsrfTokenLoader) => {
  2416. const cacheStorage = new CacheStorage(
  2417. location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage);
  2418. const TIMEOUT = 30000;
  2419. const CACHE_EXPIRE_TIME = 60 * 3 * 1000;
  2420. let isZenzaReady = false;
  2421.  
  2422. class DeflistApiLoader {
  2423.  
  2424. static getItems() {
  2425. const url = '//www.nicovideo.jp/api/deflist/list';
  2426. const cacheKey = 'deflistItems';
  2427.  
  2428. return new Promise(function(resolve, reject) {
  2429.  
  2430. const cache = cacheStorage.getItem(cacheKey);
  2431. if (cache) {
  2432. window.setTimeout(() => {
  2433. resolve({items: cache.mylistitem, status: cache.status, from: 'cache'});
  2434. }, 0);
  2435. return;
  2436. }
  2437.  
  2438. let timeoutTimer = window.setTimeout(() => {
  2439. timeoutTimer = null;
  2440. reject({status: 'fail', description: 'timeout'});
  2441. }, TIMEOUT);
  2442.  
  2443. fetch(url, {
  2444. credentials: 'include'
  2445. }).then((res) => {
  2446. return res.json();
  2447. }).then((json) => {
  2448. if (json.status !== 'ok') {
  2449. return reject(json);
  2450. }
  2451.  
  2452. if (timeoutTimer) { window.clearTimeout(timeoutTimer);
  2453. } else { return; }
  2454.  
  2455. cacheStorage.setItem(cacheKey, json, CACHE_EXPIRE_TIME);
  2456. resolve({items: json.mylistitem, status: json.status, from: 'fetch'});
  2457. });
  2458. });
  2459. }
  2460.  
  2461. static findItemByWatchId(watchId) {
  2462. return DeflistApiLoader.getItems().then(({items}) => {
  2463. for (var i = 0, len = items.length; i < len; i++) {
  2464. var item = items[i], wid = item.id || item.item_data.watch_id;
  2465. if (wid === watchId) {
  2466. return Promise.resolve(item);
  2467. }
  2468. }
  2469. return Promise.reject();
  2470. });
  2471. }
  2472.  
  2473. static _removeItem({watchId, token}) {
  2474. const cacheKey = 'deflistItems';
  2475. DeflistApiLoader.findItemByWatchId(watchId).then((item) => {
  2476. const url = '//www.nicovideo.jp/api/deflist/delete';
  2477. const body = 'id_list[0][]=' + item.item_id + '&token=' + token;
  2478.  
  2479. const req = {
  2480. credentials: 'include',
  2481. method: 'post',
  2482. body,
  2483. headers: {'Content-Type': 'application/x-www-form-urlencoded'}
  2484. };
  2485.  
  2486. return fetch(url, req)
  2487. .then(res => { return res.json(); })
  2488. .then((result) => {
  2489. if (result.status !== 'ok') {
  2490. return Promise.reject({
  2491. status: 'fail',
  2492. result: result,
  2493. code: result.error.code,
  2494. message: result.error.description
  2495. });
  2496. }
  2497.  
  2498.  
  2499. cacheStorage.removeItem(cacheKey);
  2500. util.emitter.emitAsync('deflistRemove', watchId);
  2501. return Promise.resolve({
  2502. status: 'ok',
  2503. result: result,
  2504. message: 'とりあえずマイリストから削除'
  2505. });
  2506.  
  2507. }, (err) => {
  2508. return Promise.reject({
  2509. result: err,
  2510. message: 'とりあえずマイリストから削除失敗(2)'
  2511. });
  2512. });
  2513.  
  2514. }, (err) => {
  2515. return Promise.reject({
  2516. status: 'fail',
  2517. result: err,
  2518. message: '動画が見つかりません'
  2519. });
  2520. });
  2521. }
  2522.  
  2523. static removeItem(watchId) {
  2524. return CsrfTokenLoader.load().then((token) => {
  2525. return DeflistApiLoader._removeItem({watchId, token});
  2526. });
  2527. }
  2528.  
  2529. static __addItem({watchId, description, token, isRetry = false}) {
  2530. const cacheKey = 'deflistItems';
  2531. const url = '//www.nicovideo.jp/api/deflist/add';
  2532. let body = 'item_id=' + watchId + '&token=' + token;
  2533. if (description) {
  2534. body += '&description='+ encodeURIComponent(description);
  2535. }
  2536.  
  2537. const req = {
  2538. method: 'post',
  2539. credentials: 'include',
  2540. body,
  2541. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  2542. };
  2543.  
  2544. return new Promise((resolve, reject) => {
  2545. fetch(url, req)
  2546. .then((res) => { return res.json(); })
  2547. .then((result) => {
  2548.  
  2549. if (result.status && result.status === 'ok') {
  2550. cacheStorage.removeItem(cacheKey);
  2551. //ZenzaWatch.emitter.emitAsync('deflistAdd', watchId, description);
  2552. return resolve({
  2553. status: 'ok',
  2554. result: result,
  2555. message: 'とりあえずマイリスト登録'
  2556. });
  2557. }
  2558.  
  2559. if (!result.status || !result.error) {
  2560. return reject({
  2561. status: 'fail',
  2562. result: result,
  2563. message: 'とりあえずマイリスト登録失敗(100)'
  2564. });
  2565. }
  2566.  
  2567. if (result.error.code !== 'EXIST' || isRetry) {
  2568. return reject({
  2569. status: 'fail',
  2570. result: result,
  2571. code: result.error.code,
  2572. message: result.error.description
  2573. });
  2574. }
  2575.  
  2576. /**
  2577. * すでに登録されている場合は、いったん削除して再度追加(先頭に移動)
  2578. */
  2579. return DeflistApiLoader.removeItem(watchId)
  2580. .then(util.getSleepPromise(1500, 'deflist remove'))
  2581. .then(() => {
  2582. return DeflistApiLoader._addItem(watchId, description, true)
  2583. .then((result) => {
  2584. resolve({
  2585. status: 'ok',
  2586. result: result,
  2587. message: 'とりあえずマイリストの先頭に移動'
  2588. });
  2589. });
  2590. }, (err) => {
  2591.  
  2592. reject({
  2593. status: 'fail',
  2594. result: err.result,
  2595. code: err.code,
  2596. message: 'とりあえずマイリスト登録失敗(101)'
  2597. });
  2598. });
  2599.  
  2600. }, (err) => {
  2601. reject({
  2602. status: 'fail',
  2603. result: err,
  2604. message: 'とりあえずマイリスト登録失敗(200)'
  2605. });
  2606. });
  2607. });
  2608. }
  2609.  
  2610. static _addItem(watchId, description, isRetry = false) {
  2611. return CsrfTokenLoader.load().then((token) => {
  2612. return DeflistApiLoader.__addItem({watchId, description, isRetry, token});
  2613. });
  2614. }
  2615.  
  2616. static addItem(watchId, description) {
  2617. return DeflistApiLoader._addItem(watchId, description, false);
  2618. }
  2619.  
  2620. static addItemWithOwnerName(watchId) {
  2621. return ThumbInfoLoader.loadOwnerInfo(watchId).then((owner) => {
  2622. if (!owner.id) {
  2623. return DeflistApiLoader.addItem(watchId);
  2624. }
  2625.  
  2626. const description = owner.localeName;
  2627. return DeflistApiLoader.addItem(watchId, description);
  2628. }, () => {
  2629. return DeflistApiLoader.addItem(watchId);
  2630. });
  2631. // .then(
  2632. // (result) => { console.log('ok', result); },
  2633. // (err) => { console.error('err', err); }
  2634. //);
  2635. }
  2636.  
  2637.  
  2638. static clearCache() {
  2639. cacheStorage.removeItem('deflistItems');
  2640. }
  2641.  
  2642. }
  2643.  
  2644. ZenzaDetector.detect().then((ZenzaWatch) => {
  2645. isZenzaReady = true;
  2646. ZenzaWatch.emitter.on('deflistRemove', () => {
  2647. DeflistApiLoader.clearCache();
  2648. });
  2649. });
  2650.  
  2651. //DeflistApiLoader.clearCache();
  2652.  
  2653. return DeflistApiLoader;
  2654. })(CsrfTokenLoader);
  2655.  
  2656. MylistPocket.debug.DeflistApiLoader = DeflistApiLoader;
  2657.  
  2658. class HoverMenu extends Emitter {
  2659. constructor() {
  2660. super();
  2661. this._init();
  2662. }
  2663. _init() {
  2664. this._view = document.querySelector('.mylistPocketHoverMenu');
  2665.  
  2666. this._view.addEventListener('click', this._onClick.bind(this));
  2667. this._view.addEventListener('mousedown', this._onMousedown.bind(this));
  2668. this._view.addEventListener('contextmenu', this._onContextMenu.bind(this));
  2669.  
  2670. //$('body')
  2671. // .on('mouseover', 'a[href*="watch/"],a[href*="nico.ms/"]',
  2672. // this._onHover.bind(this))
  2673. // .on('mouseover', 'a[href*="watch/"],a[href*="nico.ms/"]',
  2674. // _.debounce(this._onHoverEnd.bind(this), 500))
  2675. // .on('mouseout', 'a[href*="watch/"],a[href*="nico.ms/"]',
  2676. // this._onMouseout.bind(this));
  2677.  
  2678. document.body.addEventListener(
  2679. 'mouseover', this._onHover.bind(this), {passive: true});
  2680. document.body.addEventListener(
  2681. 'mouseout', this._onMouseout.bind(this), {passive: true});
  2682. document.body.addEventListener(
  2683. 'click', () => { this.hide(); }, {passive: true});
  2684.  
  2685. this._onHoverEnd = _.debounce(this._onHoverEnd.bind(this), 500);
  2686.  
  2687. util.emitter.on('hideHover', () => {
  2688. this.hide();
  2689. });
  2690.  
  2691. this._x = this._y = 0;
  2692.  
  2693. ZenzaDetector.detect().then((ZenzaWatch) => {
  2694. this._isZenzaReady = true;
  2695. this.addClass('is-zenzaReady');
  2696. ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => {
  2697. this.hide();
  2698. }, 1000));
  2699. });
  2700.  
  2701. this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp');
  2702. this._deflistButton = this._view.querySelector('.mylistPocketButton.deflist-add');
  2703. MylistPocket.debug.hoverMenu = this._view;
  2704. }
  2705.  
  2706. toggleClass(className, v) {
  2707. className.split(/ +/).forEach((c) => {
  2708. this._view.classList.toggle(c, v);
  2709. });
  2710. }
  2711.  
  2712. addClass(className) { this.toggleClass(className, true); }
  2713. removeClass(className) { this.toggleClass(className, false); }
  2714.  
  2715. hide() {
  2716. this.removeClass('is-show');
  2717. }
  2718.  
  2719. show() {
  2720. this.addClass('is-show');
  2721. }
  2722.  
  2723. moveTo(x, y) {
  2724. this._x = x;
  2725. this._y = y;
  2726. this._view.style.left = x + 'px';
  2727. this._view.style.top = y + 'px';
  2728. }
  2729.  
  2730. _onClick(e) {
  2731. e.preventDefault();
  2732. e.stopPropagation();
  2733. }
  2734.  
  2735. _onContextMenu(e) {
  2736. e.preventDefault();
  2737. e.stopPropagation();
  2738. }
  2739.  
  2740. _onMousedown(e) {
  2741. const watchId = this._watchId;
  2742. const target = e.target.classList.contains('command') ?
  2743. e.target : e.target.closest('.command');
  2744. const command = target.getAttribute('data-command');
  2745. e.preventDefault();
  2746. e.stopPropagation();
  2747.  
  2748. if (command === 'info') {
  2749. this._videoInfo(watchId);
  2750. this.hide();
  2751. } else if (command === 'playlist-queue') {
  2752. this.emit('playlist-queue', watchId, this);
  2753. } else {
  2754. if (e.button !== 0 || e.shiftKey) {
  2755. this._deflistRemove(watchId);
  2756. } else {
  2757. this._deflist(watchId);
  2758. }
  2759. }
  2760. }
  2761.  
  2762. _videoInfo(watchId) {
  2763. this.emit('info', watchId || this._watchId, this);
  2764. }
  2765.  
  2766. _deflist(watchId) {
  2767. this.emit('deflist-add', watchId || this._watchId, this);
  2768. }
  2769.  
  2770. _deflistRemove(watchId) {
  2771. this.emit('deflist-remove', watchId || this._watchId, this);
  2772. }
  2773.  
  2774. _onHover(e) {
  2775. const target = this._isTargetElement(e);
  2776. if (!target) { return; }
  2777.  
  2778. this._hoverElement = target;
  2779. this._onHoverEnd(e);
  2780. }
  2781.  
  2782. _onHoverEnd(e) {
  2783. const target =
  2784. e.target.tagName === 'A' ? e.target : e.target.closest('a');
  2785. if (this._hoverElement !== target) { return; }
  2786. const href = target.getAttribute('data-href') || target.getAttribute('href');
  2787. const watchId = util.getWatchId(href);
  2788. //const offset = $target.offset();
  2789. const bodyOffset = document.body.getBoundingClientRect();
  2790. const offset = target.getBoundingClientRect();
  2791. const left = offset.left - bodyOffset.left;
  2792. const top = offset.top - bodyOffset.top;
  2793. const host = target.hostname;
  2794. if (host !== 'www.nicovideo.jp' && host !== 'nico.ms') { return; }
  2795.  
  2796. if (target.classList.contains('noHoverMenu')) { return; }
  2797. if (!watchId.match(/^[a-z0-9]+$/)) { return; }
  2798. if (watchId.indexOf('lv') === 0) { return; }
  2799.  
  2800. this._watchId = watchId;
  2801. this.show();
  2802. this.moveTo(
  2803. left + target.offsetWidth - this._view.offsetWidth / 2,
  2804. top + target.offsetHeight / 2 - this._view.offsetHeight / 2
  2805. );
  2806. }
  2807.  
  2808. _onMouseout(e) {
  2809. const target = this._isTargetElement(e);
  2810. if (!target) { return; }
  2811.  
  2812. if (this._hoverElement === e.target) {
  2813. this._hoverElement = null;
  2814. }
  2815. }
  2816.  
  2817. _isTargetElement(e) {
  2818. const target =
  2819. e.target.tagName === 'A' ? e.target : e.target.closest('a');
  2820. if (!target) { return false; }
  2821. const href = target.href || '';
  2822. if (!/(watch\/[a-z0-9]+|nico\.ms\/[a-z0-9]+)/.test(href)) { return false; }
  2823. return target;
  2824. }
  2825.  
  2826. set isBusy(v) {
  2827. this._isBusy = v;
  2828. this.toggleClass('is-busy', v);
  2829. }
  2830.  
  2831. get isBusy() {
  2832. return !!this._isBusy;
  2833. }
  2834.  
  2835. notifyBeginDeflistUpdate(/*watchId*/) {
  2836. this.addClass('is-deflistUpdating');
  2837. }
  2838.  
  2839. notifyEndDeflistUpdate(result) {
  2840. this.addClass('is-deflistSuccess');
  2841. window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000);
  2842.  
  2843. //window.console.info('ok result', result);
  2844. this._deflistButton.setAttribute('data-result', result.message || '登録しました');
  2845. this.removeClass('is-deflistUpdating');
  2846. }
  2847.  
  2848. notifyFailDeflistUpdate(result) {
  2849. this.addClass('is-deflistFail');
  2850. window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
  2851.  
  2852. //window.console.info('fail result', result);
  2853. this._deflistButton.setAttribute('data-result', result.message || '登録失敗');
  2854. this.removeClass('is-deflistUpdating');
  2855. }
  2856. }
  2857.  
  2858.  
  2859. class VideoInfoView extends Emitter {
  2860. constructor({host, tpl}) {
  2861. super();
  2862. this._host = host;
  2863. this._tpl = tpl;
  2864. this._slot = {};
  2865.  
  2866. this._config = config.namespace('videoInfo');
  2867. const ngConfig = this._ngConfig = config.namespace('ng');
  2868. const favConfig = this._favConfig = config.namespace('fav');
  2869.  
  2870. const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
  2871. this._ngChecker = ngChecker;
  2872. this._favChecker = favChecker;
  2873. }
  2874.  
  2875. _initialize() {
  2876. if (this._isInitialized) { return; }
  2877. const host = this._host;
  2878. const tpl = this._tpl;
  2879.  
  2880. this._shadowRoot = util.attachShadowDom({host, tpl});
  2881. Array.prototype.forEach.call(this._host.querySelectorAll('*'), (elm) => {
  2882. //this._host.querySelectorAll('*').forEach((elm) => {
  2883. const slot = elm.getAttribute('slot');
  2884. if (!slot) { return; }
  2885. //const type = elm.getAttribute('data-type') || 'string';
  2886. this._slot[slot] = elm;
  2887. });
  2888.  
  2889. this._rootDom = this._shadowRoot.querySelector('.root');
  2890. this._hostDom = this._host;
  2891.  
  2892. this._rootDom.addEventListener('mousedown', e => { e.stopPropagation(); });
  2893. this._rootDom.querySelector('.setting-panel-main').addEventListener('click', e => {
  2894. e.stopPropagation();
  2895. });
  2896.  
  2897. this._initSettingPanel();
  2898.  
  2899. const updateNgEnable = v => { this.toggleClass('is-ng-enable', v); };
  2900. updateNgEnable(this._ngConfig.getValue('enable'));
  2901. this._ngConfig.on('enable', updateNgEnable);
  2902.  
  2903. Array.prototype.forEach.call(
  2904. this._rootDom.querySelectorAll('.scrollable'),
  2905. elm => {
  2906. elm.addEventListener('mousewheel', this._cancelMouseWheel);
  2907. });
  2908.  
  2909. this._rootDom.addEventListener('click', this._onClick.bind(this));
  2910.  
  2911. this._boundOnBodyMouseDown = this._onBodyMouseDown.bind(this);
  2912.  
  2913. MylistPocket.debug.view = this;
  2914.  
  2915. util.emitter.on('hideHover', () => {
  2916. this.hide();
  2917. });
  2918.  
  2919. const debUpdateFavNg = _.debounce(this._updateFavNg.bind(this), 100);
  2920. this._ngConfig .on('@update', debUpdateFavNg);
  2921. this._favConfig.on('@update', debUpdateFavNg);
  2922.  
  2923. ZenzaDetector.detect().then(() => {
  2924. this._isZenzaReady = true;
  2925. this.addClass('is-zenzaReady');
  2926. window.ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => {
  2927. this.hide();
  2928. }, 1000));
  2929. });
  2930. this._videoInfoArea = this._rootDom.querySelector('.video-info');
  2931. this._deflistButton =
  2932. this._rootDom.querySelector('.mylistPocketButton.deflist-add');
  2933.  
  2934. this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp');
  2935. this.toggleClass('is-firefox', util.isFirefox());
  2936. this._isInitialized = true;
  2937. }
  2938.  
  2939. _initSettingPanel() {
  2940. const onSettingFormChange = this._onSettingFormChange.bind(this);
  2941.  
  2942. const refresh = () => {
  2943. //console.log('refresh setting-form');
  2944. Array.prototype.forEach.call(
  2945. this._rootDom.querySelectorAll('.setting-form'), (elm) => {
  2946. const name = elm.getAttribute('data-config-name');
  2947. if (!name) { return; }
  2948. const namespace = elm.getAttribute('data-config-namespace') || '';
  2949. let config = this._config;
  2950. if (namespace === 'ng') { config = this._ngConfig; }
  2951. if (namespace === 'fav') { config = this._favConfig; }
  2952. const tagName = (elm.tagName.toLowerCase()).toLowerCase();
  2953. if (tagName === 'input') {
  2954. const type = (elm.type || '').toLowerCase();
  2955. switch (type) {
  2956. case 'checkbox':
  2957. elm.checked = !!config.getValue(name);
  2958. break;
  2959. default:
  2960. elm.value = config.getValue(name);
  2961. break;
  2962. }
  2963. } else if (tagName === 'select' || tagName === 'textarea') {
  2964. elm.value = config.getValue(name);
  2965. }
  2966.  
  2967. elm.removeEventListener('change', onSettingFormChange);
  2968. elm.addEventListener('change', onSettingFormChange);
  2969. });
  2970. };
  2971.  
  2972. const onUpdate = _.debounce(refresh, 100);
  2973.  
  2974. const syncZenza = _.debounce(() => {
  2975. if (!this._ngConfig.getValue('syncZenza') || !this._isZenzaReady) { return; }
  2976. window.ZenzaWatch.config.setValue(
  2977. 'videoTagFilter', this._ngConfig.getValue('tag'));
  2978. window.ZenzaWatch.config.setValue(
  2979. 'videoOwnerFilter', this._ngConfig.getValue('owner'));
  2980. }, 1000);
  2981.  
  2982. refresh();
  2983.  
  2984. this._config .on('@update', onUpdate);
  2985. this._favConfig.on('@update', onUpdate);
  2986. this._ngConfig.on('@update', () => {
  2987. onUpdate();
  2988. syncZenza();
  2989. });
  2990.  
  2991. }
  2992.  
  2993. _onSettingFormChange(e) {
  2994. const elm = e.target;
  2995. const name = elm.getAttribute('data-config-name');
  2996. if (!name) { return; }
  2997. const namespace = elm.getAttribute('data-config-namespace') || '';
  2998. let config = this._config;
  2999. if (namespace === 'ng') { config = this._ngConfig; }
  3000. if (namespace === 'fav') { config = this._favConfig; }
  3001.  
  3002. const tagName = (elm.tagName.toLowerCase()).toLowerCase();
  3003. if (tagName === 'input') {
  3004. const type = (elm.type || '').toLowerCase();
  3005. switch (type) {
  3006. case 'checkbox':
  3007. config.setValue(name, elm.checked);
  3008. break;
  3009. default:
  3010. config.setValue(name, elm.value);
  3011. break;
  3012. }
  3013. } else if (tagName === 'select' || tagName === 'textarea') {
  3014. config.setValue(name, elm.value);
  3015. }
  3016. }
  3017.  
  3018. toggleClass(className, v) {
  3019. className.split(/ +/).forEach((c) => {
  3020. this._rootDom.classList.toggle(c, v);
  3021. this._hostDom.classList.toggle(c, v);
  3022. });
  3023. }
  3024.  
  3025. addClass(className) { this.toggleClass(className, true); }
  3026. removeClass(className) { this.toggleClass(className, false); }
  3027. bind(videoInfo) {
  3028. this._videoInfo = videoInfo;
  3029. //console.info('status?', videoInfo.status, videoInfo.status === 'ok');
  3030. if (videoInfo.status === 'ok') {
  3031. this._bindSuccess(videoInfo);
  3032. } else {
  3033. this._bindFail(videoInfo);
  3034. }
  3035. window.setTimeout(() => {
  3036. this.removeClass('is-loading');
  3037. }, 0);
  3038. }
  3039.  
  3040. _onClick(e) {
  3041. const t = e.target;
  3042. const elm =
  3043. t.classList.contains('command') ?
  3044. t : e.target.closest('.command');
  3045. if (!elm) { return; }
  3046.  
  3047. // 簡易 throttle
  3048. if (elm.classList.contains('is-active')) { return; }
  3049. elm.classList.add('is-active');
  3050. window.setTimeout(() => { elm.classList.remove('is-active'); }, 500);
  3051.  
  3052. e.preventDefault();
  3053. e.stopPropagation();
  3054. const command = elm.getAttribute('data-command');
  3055. const param = elm.getAttribute('data-param');
  3056. switch (command) {
  3057. case 'toggle-setting':
  3058. this.toggleSettingPanel();
  3059. break;
  3060. case 'add-ng-tag': case 'add-fav-tag':
  3061. case 'toggle-ng-tag': case 'toggle-fav-tag': {
  3062. const tag = elm.getAttribute('data-tag') || '';
  3063. if (!tag) { break; }
  3064. this.emit('command', command, {
  3065. watchId: this._videoInfo.watchId,
  3066. value: tag
  3067. }, this);
  3068. }
  3069. break;
  3070. case 'add-ng-owner': case 'add-fav-owner':
  3071. case 'toggle-ng-owner': case 'toggle-fav-owner': {
  3072. let owner =
  3073. (this._videoInfo.isChannel ? 'ch' : '') +
  3074. this._videoInfo.ownerId + '#' + this._videoInfo.ownerName;
  3075. this.emit('command', command, {
  3076. watchId: this._videoInfo.watchId,
  3077. value: owner
  3078. }, this);
  3079. }
  3080. break;
  3081. default:
  3082. this.emit('command', command, param, this);
  3083. }
  3084. }
  3085.  
  3086. _updateFavNg() {
  3087. if (!this._isInitialized) { return; }
  3088. if (!this._videoInfo || this._videoInfo.status !== 'ok') { return; }
  3089.  
  3090. const videoInfo = this._videoInfo;
  3091. const ownerInfo = this._rootDom.querySelector('.owner-info');
  3092. ownerInfo.classList.toggle('is-favorited',
  3093. this._favChecker.isMatchOwner(videoInfo.owner));
  3094. ownerInfo.classList.toggle('is-ng',
  3095. this._ngChecker .isMatchOwner(videoInfo.owner));
  3096.  
  3097. Array.prototype.forEach.call(
  3098. this._rootDom.querySelectorAll('.tag-container'),
  3099. (elm) => {
  3100. const tag = elm.getAttribute('data-tag');
  3101. elm.classList.toggle('is-favorited', this._favChecker.isMatchTag(tag));
  3102. elm.classList.toggle('is-ng', this._ngChecker.isMatchTag(tag));
  3103. });
  3104. }
  3105.  
  3106. _cancelMouseWheel(e) {
  3107. const target = this;
  3108. const delta = e.wheelDelta;
  3109. const scrollTop = target.scrollTop;
  3110. const height = target.offsetHeight;
  3111. const scrollHeight = target.scrollHeight;
  3112. if (delta > 0) { // up
  3113. if (scrollTop <= delta) {
  3114. target.scrollTop = 0;
  3115. e.preventDefault(); e.stopPropagation();
  3116. }
  3117. } else if (scrollTop >= scrollHeight - height + delta) { // down
  3118. target.scrollTop = scrollHeight - height;
  3119. e.preventDefault(); e.stopPropagation();
  3120. }
  3121. }
  3122.  
  3123. toggleSettingPanel() {
  3124. this.toggleClass('is-setting');
  3125. }
  3126.  
  3127. _onBodyMouseDown() {
  3128. document.body.removeEventListener('mousedown', this._boundOnBodyMouseDown);
  3129. this.hide();
  3130. }
  3131.  
  3132. reset() {
  3133. this._initialize();
  3134. this._videoInfoArea.scrollTop = 0;
  3135. this.removeClass('noclip');
  3136. this.addClass('is-loading');
  3137. }
  3138.  
  3139. show() {
  3140. this.addClass('show');
  3141. document.body.addEventListener('mousedown', this._boundOnBodyMouseDown);
  3142. }
  3143.  
  3144. hide() {
  3145. this.removeClass('show is-ok is-fail noclip is-setting');
  3146. }
  3147. _bindSuccess(videoInfo) {
  3148. const toCamel = p => {
  3149. return p.replace(/-./g, s => { return s.charAt(1).toUpperCase(); });
  3150. };
  3151.  
  3152. Object.keys(this._slot).forEach((key) => {
  3153. const camelKey = toCamel(key);
  3154. const data = videoInfo[camelKey];
  3155. //console.log('keys', typeof data, key, camelKey, data);
  3156. if (typeof data !== 'string' && typeof data !== 'object') { return; }
  3157.  
  3158. const elm = this._slot[key];
  3159. const type = elm.getAttribute('data-type') || 'string';
  3160.  
  3161. switch (type) {
  3162. case 'html':
  3163. this._createDescription(elm, data);
  3164. break;
  3165. case 'int':
  3166. let i = parseInt(data, 10);
  3167. i = i.toLocaleString ? i.toLocaleString() : i;
  3168. elm.textContent = i;
  3169. break;
  3170. case 'link':
  3171. elm.href = data;
  3172. break;
  3173. case 'image':
  3174. elm.src = data;
  3175. break;
  3176. case 'date':
  3177. elm.textContent = data.toLocaleString();
  3178. break;
  3179. default:
  3180. elm.textContent = data;
  3181. }
  3182. });
  3183.  
  3184. const df = document.createDocumentFragment();
  3185. //Array.prototype.forEach.call(this._host.querySelectorAll('.tag'), t => { t.remove(); });
  3186. videoInfo.tags.forEach(tag => { df.appendChild((this._createTagSlot(tag))); });
  3187. const videoTags = this._rootDom.querySelector('.video-tags');
  3188. videoTags.innerHTML = '';
  3189. videoTags.appendChild(df);
  3190.  
  3191. Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-watch-id'), elm => {
  3192. elm.setAttribute('data-param', videoInfo.watchId);
  3193. });
  3194. Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-video-id'), elm => {
  3195. elm.setAttribute('data-param', videoInfo.videoId);
  3196. });
  3197.  
  3198. const target = this._config.getValue('openNewWindow') ? '_blank' : '_self';
  3199. Array.prototype.forEach.call(
  3200. this._host.querySelectorAll('.target-change'), elm => {
  3201. elm.target = target;
  3202. elm.rel = 'noopener';
  3203. });
  3204.  
  3205. this._updateFavNg();
  3206.  
  3207. this.toggleClass('is-channel', videoInfo.isChannel);
  3208. this.addClass('is-ok');
  3209. this.removeClass('is-fail');
  3210. window.setTimeout(() => { this.addClass('noclip'); }, 1000);
  3211. }
  3212.  
  3213. _createDescription(elm, data) {
  3214. elm.innerHTML = util.httpLink(data);
  3215. const watchReg = /watch\/([a-z0-9]+)/;
  3216. const isZenzaReady = this._isZenzaReady;
  3217. //if (util.isFirefox()) { return; }
  3218. Array.from(elm.querySelectorAll('.videoLink[href*=\'watch/\']')).forEach((link) => {
  3219. const href = link.getAttribute('href');
  3220. if (!watchReg.test(href)) { return; }
  3221. const watchId = RegExp.$1;
  3222. if (isZenzaReady) {
  3223. link.classList.add('noHoverMenu');
  3224. link.classList.add('command');
  3225. link.setAttribute('data-command', 'zenza-open');
  3226. link.setAttribute('data-param', watchId);
  3227. }
  3228. const btn = document.createElement('button');
  3229. btn.innerHTML = '?';
  3230. btn.className = 'command command-button noHoverMenu';
  3231. btn.setAttribute('slot', 'command-button');
  3232. btn.setAttribute('tooltip', '動画情報');
  3233. btn.setAttribute('data-command', 'info');
  3234. btn.setAttribute('data-param', watchId);
  3235. link.appendChild(btn);
  3236.  
  3237. const thumbnail = util.getThumbnailUrlByVideoId(watchId);
  3238. if (thumbnail) {
  3239. const img = document.createElement('img');
  3240. img.className = 'videoThumbnail';
  3241. img.src = thumbnail;
  3242. link.classList.add('popupThumbnail');
  3243. link.appendChild(img);
  3244. }
  3245. link.classList.add('watch');
  3246. });
  3247. }
  3248.  
  3249. _bindFail(videoInfo) {
  3250. this._slot['error-description'].textContent =
  3251. `動画情報の取得に失敗しました (${videoInfo.description})`;
  3252. this.addClass('is-fail');
  3253. this.removeClass('is-ok');
  3254. }
  3255.  
  3256.  
  3257. _createTagSlot(tag) {
  3258. const text = util.escapeHtml(tag.text);
  3259. const lock = tag.isLocked ? 'is-locked' : '';
  3260. const span = document.createElement('span');
  3261.  
  3262. const a = document.createElement('a');
  3263. const target = this._config.getValue('openNewWindow') ? '_blank' : '_self';
  3264. a.textContent = tag.text;
  3265. a.className = `tag ${lock}`;
  3266. a.target = target;
  3267. a.rel = 'noopener';
  3268. a.href = `/tag/${encodeURIComponent(text)}`;
  3269. span.appendChild(a);
  3270.  
  3271. const fav = document.createElement('button');
  3272. fav.className = 'add-fav-button command';
  3273. fav.setAttribute('data-command', 'toggle-fav-tag');
  3274. fav.setAttribute('data-tag', tag.text);
  3275. fav.innerHTML = '★'; //'&#8416;'; // &#x2716;
  3276. span.appendChild(fav);
  3277.  
  3278. const bt = document.createElement('button');
  3279. bt.className = 'add-ng-button command';
  3280. bt.setAttribute('data-command', 'toggle-ng-tag');
  3281. bt.setAttribute('data-tag', tag.text);
  3282. bt.innerHTML = '&#x2716;'; //'&#8416;'; // &#x2716;
  3283. span.appendChild(bt);
  3284.  
  3285. span.className = 'tag-container';
  3286. span.setAttribute('data-tag', tag.text);
  3287. span.slot = 'tag';
  3288. return span;
  3289. }
  3290.  
  3291. notifyBeginDeflistUpdate(/*watchId*/) {
  3292. this.addClass('is-deflistUpdating');
  3293. }
  3294.  
  3295. notifyEndDeflistUpdate(result) {
  3296. this.addClass('is-deflistSuccess');
  3297. window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000);
  3298.  
  3299. //window.console.info('ok result', result);
  3300. this._deflistButton.setAttribute('data-result', result.message || '登録しました');
  3301. this.removeClass('is-deflistUpdating');
  3302. }
  3303.  
  3304. notifyFailDeflistUpdate(result) {
  3305. this.addClass('is-deflistFail');
  3306. window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
  3307.  
  3308. //window.console.info('fail result', result);
  3309. this._deflistButton.setAttribute('data-result', result.message || '登録失敗');
  3310. this.removeClass('is-deflistUpdating');
  3311. }
  3312. }
  3313.  
  3314.  
  3315. class VideoInfo {
  3316. static createByThumbInfo({xml, watchId}) {
  3317. const dom = (new DOMParser()).parseFromString(xml, 'text/xml');
  3318. const status =
  3319. dom.getElementsByTagName('nicovideo_thumb_response')[0].getAttribute('status');
  3320. //console.info('status', status);
  3321. const t = function(name) {
  3322. const tt = dom.getElementsByTagName(name);
  3323. if (!tt || !tt[0]) {
  3324. return '';
  3325. }
  3326. return tt[0].textContent.trim();
  3327. };
  3328.  
  3329. const videoId = t('video_id');
  3330. let thumbnail = t('thumbnail_url');
  3331. if (util.hasLargeThumbnail(videoId)) {
  3332. thumbnail += '.L';
  3333. }
  3334.  
  3335. const isChannel = !!t('ch_id');
  3336. const tags = [];
  3337. const rawData = {
  3338. status,
  3339. videoId: t('video_id'),
  3340. watchId: watchId,
  3341. videoTitle: t('title'),
  3342. videoThumbnail: thumbnail,
  3343. uploadDate: t('first_retrieve'),
  3344. duration: t('length'),
  3345. viewCounter: t('view_counter'),
  3346. mylistCounter: t('mylist_counter'),
  3347. commentCounter: t('comment_num'),
  3348. description: t('description'),
  3349. lastResBody: t('last_res_body'),
  3350. isChannel,
  3351. ownerId: isChannel ? t('ch_id') : t('user_id'),
  3352. ownerName: isChannel ? t('ch_name') : t('user_nickname'),
  3353. ownerIcon: isChannel ? t('ch_icon_url') : t('user_icon_url'),
  3354. tags
  3355. };
  3356.  
  3357. dom.querySelectorAll('tag').forEach(tag => {
  3358. const isLocked = tag.getAttribute('lock');
  3359. const text = tag.textContent;
  3360. tags.push({text, isLocked});
  3361. });
  3362.  
  3363. return new VideoInfo(rawData);
  3364. }
  3365.  
  3366. constructor(rawData) {
  3367. this._rawData = rawData;
  3368. }
  3369.  
  3370. get status() { return this._rawData.status; }
  3371. get videoId() { return this._rawData.videoId; }
  3372. get watchId() { return this._rawData.watchId; }
  3373. get videoTitle() { return this._rawData.videoTitle; }
  3374. get videoThumbnail() { return this._rawData.videoThumbnail; }
  3375. get description() { return this._rawData.description; }
  3376. get duration() { return this._rawData.duration; }
  3377. get owner() {
  3378. return {
  3379. type: this.isChannel ? 'channel' : 'user',
  3380. id: this.ownerId,
  3381. name: this.ownerName,
  3382. icon: this.ownerIcon
  3383. };
  3384. }
  3385.  
  3386. get ownerPageLink() {
  3387. const ownerId = this.ownerId;
  3388. if (this.isChannel) {
  3389. return `//ch.nicovideo.jp/ch${ownerId}`;
  3390. } else {
  3391. return `//www.nicovideo.jp/user/${ownerId}`;
  3392. }
  3393. }
  3394. get ownerIcon() { return this._rawData.ownerIcon; }
  3395. get ownerName() { return this._rawData.ownerName; }
  3396. get localeOwnerName() {
  3397. if (this.isChannel) {
  3398. return this.ownerName;
  3399. } else {
  3400. // TODO: 言語依存
  3401. return this.ownerName + ' さん';
  3402. }
  3403. }
  3404. get ownerId() { return this._rawData.ownerId; }
  3405. get isChannel() { return this._rawData.isChannel; }
  3406. get uploadDate() { return new Date(this._rawData.uploadDate); }
  3407.  
  3408. get viewCounter() { return this._rawData.viewCounter; }
  3409. get mylistCounter() { return this._rawData.mylistCounter; }
  3410. get commentCounter() { return this._rawData.commentCounter; }
  3411.  
  3412. get lastResBody() { return this._rawData.lastResBody; }
  3413. get tags() { return this._rawData.tags; }
  3414. }
  3415.  
  3416.  
  3417.  
  3418. const deflistAdd = (watchId) => {
  3419. if (location.host === 'www.nicovideo.jp') {
  3420. return DeflistApiLoader.addItemWithOwnerName(watchId);
  3421. }
  3422.  
  3423. let zenza;
  3424. let token;
  3425. return ZenzaDetector.detect().then((z) => {
  3426. zenza = z;
  3427. }).then(() => {
  3428. return CsrfTokenLoader.load().then((t) => {
  3429. token = t;
  3430. }, () => { return Promise.resolve(); });
  3431. }).then(() => {
  3432. return ThumbInfoLoader.loadOwnerInfo(watchId);
  3433. }).then((owner) => {
  3434. //console.info(watchId, token, owner, zenza);
  3435. if (!owner.id) {
  3436. return zenza.external.deflistAdd(watchId);
  3437. }
  3438.  
  3439. const description = owner.localeName;
  3440. return zenza.external.deflistAdd({watchId, description, token});
  3441. });
  3442. };
  3443.  
  3444. const deflistRemove = (watchId) => {
  3445. if (location.host === 'www.nicovideo.jp') {
  3446. return DeflistApiLoader.removeItem(watchId);
  3447. }
  3448.  
  3449. let zenza;
  3450. let token;
  3451. return ZenzaDetector.detect().then((z) => {
  3452. zenza = z;
  3453. }).then(() => {
  3454. return CsrfTokenLoader.load().then((t) => {
  3455. token = t;
  3456. }, () => { return Promise.resolve(); });
  3457. }).then(() => {
  3458. return zenza.external.deflistRemove({watchId, token});
  3459. });
  3460. };
  3461.  
  3462.  
  3463.  
  3464. class MatchChecker {
  3465. constructor({word = '', tag = '', owner = ''}) {
  3466. this.init({word, tag, owner});
  3467. }
  3468.  
  3469. init({word, tag, owner}) {
  3470. this._tag = [];
  3471. tag.split(/[\r\n]+/).forEach((t) => {
  3472. if (t) { this._tag.push(t.trim()); }
  3473. });
  3474. this._tag = _.uniq(this._tag);
  3475.  
  3476. let wordTmp = [];
  3477. this._word = null;
  3478. word.split(/[\r\n]+/).forEach((w) => {
  3479. if (w) { wordTmp.push(util.escapeRegs(w.trim())); }
  3480. });
  3481. wordTmp = _.uniq(wordTmp);
  3482. if (wordTmp.length > 0) {
  3483. this._word = new RegExp('(' + wordTmp.join('|') + ')', 'i');
  3484. }
  3485. //console.info('word', word, wordTmp.length, this._word);
  3486.  
  3487. this._userId = [];
  3488. this._channelId = [];
  3489. owner.split(/[\r\n]+/).forEach((o) => {
  3490. if (typeof o === 'string') {
  3491. const id = o.split('#')[0].trim();
  3492. if (id.startsWith('ch')) {
  3493. this._channelId.push(parseInt(id.substring(2)));
  3494. } else {
  3495. this._userId.push(parseInt(id));
  3496. }
  3497. }
  3498. });
  3499. this._userId = _.uniq(this._userId);
  3500. this._channelId = _.uniq(this._channelId);
  3501.  
  3502. //console.info('ng', this._tag, this._word, this._userId, this._channelId);
  3503. }
  3504.  
  3505. isMatch(data) {
  3506. if (this._isMatchTag(data.tagList)) { return true; }
  3507. if (this._isMatchOwner(data.owner)) { return true; }
  3508. if (this._isMatchWord({title: data.title, description: data.description})) { return true; }
  3509. }
  3510.  
  3511. _isMatchTag(tagList = []) {
  3512. if (this._tag.length < 1) { return false; }
  3513.  
  3514. const tagTmp = [];
  3515. tagList.forEach(t => { if (t) { tagTmp.push(util.escapeRegs(t.trim())); } });
  3516. const tagReg = new RegExp(' (' + tagTmp.join('|') + ') ', 'i');
  3517. const _tag = ' ' + this._tag.join(' ') + ' ';
  3518. return tagReg.test(_tag);
  3519. }
  3520.  
  3521. _isMatchOwner(owner) {
  3522. const _id = owner.type === 'user' ? this._userId : this._channelId;
  3523. return _.contains(_id, parseInt(owner.id, 10));
  3524. }
  3525.  
  3526. _isMatchWord({title, description}) {
  3527. if (!this._word) { return false; }
  3528. //console.log(title, this._word.test(title));
  3529. //console.log(description, this._word.test(description));
  3530. return this._word.test(title) || this._word.test(description);
  3531. }
  3532.  
  3533. isMatchTag(tag) {
  3534. return this._isMatchTag([tag]);
  3535. }
  3536.  
  3537. isMatchOwner(owner) {
  3538. return this._isMatchOwner(owner);
  3539. }
  3540. }
  3541.  
  3542. class NgChecker extends MatchChecker {
  3543. isNg(data) {
  3544. return super.isMatch(data);
  3545. }
  3546. }
  3547.  
  3548. const initDom = () => {
  3549. util.addStyle(__css__);
  3550. const f = document.createElement('div');
  3551. f.id = 'mylistPocketDomContainer';
  3552. f.innerHTML = __tpl__;
  3553. document.body.appendChild(f);
  3554. };
  3555.  
  3556. const initZenzaBridge = () => {
  3557. ZenzaDetector.initialize();
  3558. };
  3559.  
  3560. const createVideoInfoView = () => {
  3561. const host = document.getElementById('mylistPocket-popup');
  3562. const tpl = document.getElementById('mylistPocket-popup-template');
  3563. const vv = new VideoInfoView({host, tpl});
  3564. return vv;
  3565. };
  3566.  
  3567. const createVideoInfoLoader = (vv) => {
  3568.  
  3569. const onVideoInfoLoad = ({data, watchId}) => {
  3570. const vi = VideoInfo.createByThumbInfo({xml: data, watchId});
  3571. vv.bind(vi);
  3572. };
  3573.  
  3574. const onVideoInfoFail = () => {
  3575. vv.bind({status: 'fail', description: '通信失敗'});
  3576. return Promise.resolve();
  3577. };
  3578.  
  3579. return function(watchId) {
  3580. vv.reset();
  3581. vv.show();
  3582. return ThumbInfoLoader.loadXml(watchId).then(
  3583. onVideoInfoLoad, onVideoInfoFail
  3584. );
  3585. };
  3586. };
  3587.  
  3588. const createCommandDispatcher = ({infoView}) => {
  3589. const load = createVideoInfoLoader(infoView);
  3590.  
  3591. const ngConfig = config.namespace('ng');
  3592. const favConfig = config.namespace('fav');
  3593.  
  3594. const toggleFavNg = (command, param) => {
  3595. let [cmd, namespace, key] = command.split('-');
  3596. let _config = namespace === 'fav' ? favConfig : ngConfig;
  3597. _config.refresh();
  3598. const value = param.value.trim();
  3599. let ngs = _config.getValue(key).trim().split(/[\r\n]/);
  3600. const isContain = _.contains(ngs, value);
  3601.  
  3602. if (isContain || cmd === 'remove') {
  3603. ngs = ngs.filter((line) => {
  3604. if (line === value) {
  3605. window.console.info('%c-%s:%s', 'background: cyan', key, value);
  3606. }
  3607. return line !== value;
  3608. });
  3609. cmd = 'remove';
  3610. } else if (!isContain || cmd === 'add') {
  3611. ngs.push(value);
  3612. window.console.info('%c+%s:%s', 'background: cyan', key, value);
  3613. cmd = 'add';
  3614. }
  3615.  
  3616. ngs = _.uniq(ngs);
  3617.  
  3618. _config.setValue(key, ngs.join('\n').trim());
  3619.  
  3620. const className = namespace === 'fav' ? 'is-fav-favorited' : 'is-ng-rejected';
  3621. Array.prototype.forEach.call(
  3622. document.querySelectorAll(`*[data-watch-id=${param.watchId}]`),
  3623. item => { item.classList.toggle(className, cmd === 'add'); });
  3624. };
  3625.  
  3626. return (command, param, src) => {
  3627. switch(command) {
  3628. case 'info':
  3629. return load(param);
  3630. case 'mylist-window':
  3631. window.open(
  3632. '//www.nicovideo.jp/mylist_add/video/' + param,
  3633. 'nicomylistadd',
  3634. 'width=500, height=400, menubar=no, scrollbars=no');
  3635. break;
  3636. case 'twitter-hash-open':
  3637. window.open('https://twitter.com/hashtag/' + param + '?src=hash');
  3638. break;
  3639. case 'zenza-open-now':
  3640. window.ZenzaWatch.external.sendOrExecCommand('openNow', param);
  3641. break;
  3642. case 'zenza-open':
  3643. window.ZenzaWatch.external.sendOrOpen(param);
  3644. break;
  3645. case 'playlist-inert':
  3646. window.ZenzaWatch.external.playlist.insert(param);
  3647. break;
  3648. case 'playlist-queue':
  3649. window.ZenzaWatch.external.playlist.add(param);
  3650. break;
  3651. case 'deflist-add':
  3652. src.notifyBeginDeflistUpdate('is-deflistUpdating');
  3653.  
  3654. return deflistAdd(param)
  3655. .then(util.getSleepPromise(1000, 'deflist-add'))
  3656. .then((result) => {
  3657. src.notifyEndDeflistUpdate(result);
  3658. }, (err) => {
  3659. console.error('deflist-add-result', err);
  3660. src.notifyFailDeflistUpdate(err);
  3661. });
  3662. case 'deflist-remove':
  3663. src.notifyBeginDeflistUpdate('is-deflistUpdating');
  3664.  
  3665. return deflistRemove(param)
  3666. .then(util.getSleepPromise(1000, 'deflist-remove'))
  3667. .then(() => {
  3668. src.notifyEndDeflistUpdate({message: '削除しました'});
  3669. }, (err) => {
  3670. console.error('deflist-remove-result', err);
  3671. src.notifyFailDeflistUpdate(err);
  3672. });
  3673. case 'add-ng-word': case 'add-ng-tag': case 'add-ng-owner':
  3674. case 'add-fav-word': case 'add-fav-tag': case 'add-fav-owner':
  3675. case 'remove-ng-word': case 'remove-ng-tag': case 'remove-ng-owner':
  3676. case 'remove-fav-word': case 'remove-fav-tag': case 'remove-fav-owner':
  3677. case 'toggle-ng-word': case 'toggle-ng-tag': case 'toggle-ng-owner':
  3678. case 'toggle-fav-word': case 'toggle-fav-tag': case 'toggle-fav-owner':
  3679. toggleFavNg(command, param);
  3680. break;
  3681. }
  3682. };
  3683. };
  3684.  
  3685. const initExternal = (dispatcher, hoverMenu, infoView) => {
  3686. MylistPocket.external = {
  3687. info: (watchId) => { dispatcher('info', watchId); },
  3688. hide: () => {
  3689. hoverMenu.hide();
  3690. infoView.hide();
  3691. }
  3692. };
  3693.  
  3694. MylistPocket.isReady = true;
  3695.  
  3696. const ev = new CustomEvent('MylistPocketInitialized', { detail: { MylistPocket } });
  3697. document.body.dispatchEvent(ev);
  3698. // 過去の互換用
  3699. if (window.jQuery) {
  3700. window.jQuery('body').trigger('MylistPocketReady', MylistPocket);
  3701. }
  3702. };
  3703.  
  3704.  
  3705. const QueueLoader = (() => {
  3706. let lastPromise = null;
  3707. let count = 0;
  3708. const MAX_LOAD = 6;
  3709. const promises = [];
  3710.  
  3711. const load = function(watchId, item) {
  3712. count = (count + 1) % MAX_LOAD;
  3713. lastPromise = promises[count];
  3714. //console.time('load-' + watchId);
  3715. const onLoad = (result) => {
  3716. //console.timeEnd('load-' + watchId);
  3717. if (item) {
  3718. item.setAttribute('data-watch-id', watchId);
  3719. item.setAttribute('data-thumb-info', JSON.stringify(result));
  3720. }
  3721. const sleepTime = result.data.fromCache ? 0 : 50;
  3722. return (util.getSleepPromise(sleepTime, 'success-' + watchId))(result);
  3723. };
  3724. const onFail = util.getSleepPromise(1000, 'fail-' + watchId);
  3725.  
  3726. if (!lastPromise) {
  3727. if (item) { item.classList.add('is-ng-current'); }
  3728. lastPromise = ThumbInfoLoader.load(watchId).then(onLoad, onFail);
  3729. } else {
  3730. //lastPromise = Promise.all([lastPromise]).then(() => {
  3731. lastPromise = Promise.race(promises).then(() => {
  3732. if (item) { item.classList.add('is-ng-current'); }
  3733. return ThumbInfoLoader.load(watchId).then(onLoad, onFail);
  3734. });
  3735. }
  3736.  
  3737. promises[count] = lastPromise;
  3738. return lastPromise;
  3739. };
  3740.  
  3741. return {
  3742. load
  3743. };
  3744. })();
  3745.  
  3746. const getNgEnv = () => {
  3747. if (location.host === 'www.nicovideo.jp' &&
  3748. (location.pathname.startsWith('/ranking') ||
  3749. location.pathname.startsWith('/tag') ||
  3750. location.pathname.startsWith('/search'))
  3751. ) {
  3752. return {
  3753. query: '.item[data-id]:not(.is-ng-wait), .item_cell[data-id]:not(.is-ng-wait)',
  3754. container: document.querySelector('.column.main, .container.column1024-0')
  3755. };
  3756. }
  3757.  
  3758. if (location.host === 'ch.nicovideo.jp' &&
  3759. location.pathname.startsWith('/search')) {
  3760. return {
  3761. query: '.item:not(.is-ng-wait)',
  3762. container: document.querySelector('.site_body')
  3763. };
  3764. }
  3765.  
  3766. if (location.host === 'search.nicovideo.jp') {
  3767. return {
  3768. query: '.video:not(.is-ng-wait)',
  3769. container: document.querySelector('#row-results')
  3770. };
  3771. }
  3772.  
  3773.  
  3774. return {query: null, container: null};
  3775. };
  3776.  
  3777. const initNgConfig = () => {
  3778. const ngConfig = config.namespace('ng');
  3779. const updateEnable = v => { document.body.classList.toggle('is-ng-disable', !v); };
  3780. updateEnable(ngConfig.getValue('enable'));
  3781. if (!ngConfig.getValue('enable')) { return {}; }
  3782. ngConfig.on('enable', updateEnable);
  3783.  
  3784. const favConfig = config.namespace('fav');
  3785. return {ngConfig, favConfig};
  3786. };
  3787.  
  3788. const initNgChecker = ({ngConfig, favConfig}) => {
  3789. const ngChecker = new NgChecker({
  3790. word: ngConfig.getValue('word'),
  3791. tag: ngConfig.getValue('tag'),
  3792. owner: ngConfig.getValue('owner')
  3793. });
  3794.  
  3795. ngConfig.on('@update', _.debounce(({key, value}) => {
  3796. //console.info('ngConfig updated: ', key, value);
  3797. ngChecker.init({
  3798. word: ngConfig.getValue('word'),
  3799. tag: ngConfig.getValue('tag'),
  3800. owner: ngConfig.getValue('owner')
  3801. });
  3802. }, 100));
  3803.  
  3804.  
  3805. const favChecker = new MatchChecker({
  3806. word: favConfig.getValue('word'),
  3807. tag: favConfig.getValue('tag'),
  3808. owner: favConfig.getValue('owner')
  3809. });
  3810.  
  3811. favConfig.on('@update', _.debounce(({key, value}) => {
  3812. //console.info('favConfig updated: ', key, value);
  3813. favChecker.init({
  3814. word: favConfig.getValue('word'),
  3815. tag: favConfig.getValue('tag'),
  3816. owner: favConfig.getValue('owner')
  3817. });
  3818. }, 100));
  3819.  
  3820. return {ngChecker, favChecker};
  3821. };
  3822.  
  3823. const initIntersectionObserver = (onInview) => {
  3824.  
  3825. const onItemInview = (item) => {
  3826. let watchId = item.getAttribute('data-id');
  3827. if (!watchId) {
  3828. const a = item.querySelector("a[href*='watch/']");
  3829. if (!a) { return; }
  3830. if (!/^\/watch\/([a-z0-9]+)/.test(a.pathname)) { return; }
  3831. watchId = RegExp.$1;
  3832. }
  3833.  
  3834. if (!watchId) { return; }
  3835.  
  3836. item.classList.add('is-ng-queue');
  3837. onInview(item, watchId);
  3838. };
  3839.  
  3840. const intersectionObserver = new window.IntersectionObserver(entries => {
  3841. entries.forEach(entry => {
  3842. const item = entry.target;
  3843. intersectionObserver.unobserve(item);
  3844. onItemInview(item);
  3845. });
  3846. }, { rootMargin: '400px'});
  3847.  
  3848. return intersectionObserver;
  3849. };
  3850.  
  3851. const initNgDom = ({intersectionObserver, query, container}) => {
  3852.  
  3853. //console.info('initNgDom', intersectionObserver, query, container);
  3854. if (!container) { return; }
  3855. util.addStyle(__ng_css__);
  3856.  
  3857. const update = () => {
  3858. const items = document.querySelectorAll(query);
  3859. if (!items || items.length < 1) { return; }
  3860. Array.prototype.forEach.call(items, (item) => {
  3861. //if (item.offsetLeft < 0) { return; }
  3862. item.classList.add('is-ng-wait');
  3863. intersectionObserver.observe(item);
  3864. });
  3865. };
  3866. update();
  3867.  
  3868. const onUpdate = _.throttle(update, 1000);
  3869.  
  3870. if (!container) { return; }
  3871. const mutationObserver = new window.MutationObserver((mutations) => {
  3872. let isAdded = false;
  3873. mutations.forEach(mutation => {
  3874. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  3875. isAdded = true;
  3876. }
  3877. });
  3878. if (isAdded) { onUpdate(); }
  3879. });
  3880.  
  3881. mutationObserver.observe(
  3882. container,
  3883. {childList: true, characterData: false, attributes: false, subtree: true}
  3884. );
  3885. };
  3886.  
  3887. const initNg = () => {
  3888. if (!window.IntersectionObserver) { return; }
  3889.  
  3890. const {query, container} = getNgEnv();
  3891. if (!query) { return; }
  3892.  
  3893. const {ngConfig, favConfig} = initNgConfig();
  3894. if (!ngConfig) { return; }
  3895.  
  3896. const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
  3897.  
  3898. const onItemInview = function(item, watchId) {
  3899.  
  3900. const loadLazy = () => {
  3901. const lazyImage = item.querySelector('.jsLazyImage');
  3902. if (lazyImage) {
  3903. const origImage = lazyImage.getAttribute('data-original');
  3904. if (origImage) {
  3905. lazyImage.src = origImage;
  3906. lazyImage.classList.remove('jsLazyImage');
  3907. }
  3908. }
  3909. };
  3910.  
  3911. QueueLoader.load(watchId, item).then(
  3912. (result) => {
  3913. item.classList.remove('is-ng-current');
  3914. if (!result || !result.data) {
  3915. console.error('empty data', watchId, result);
  3916. item.classList.add('is-ng-failed');
  3917. } else {
  3918. item.classList.add(
  3919. ngChecker.isNg(result.data) ? 'is-ng-rejected' : 'is-ng-resolved');
  3920. if (favChecker.isMatch(result.data)) {
  3921. item.classList.add('is-fav-favorited');
  3922. }
  3923. }
  3924. loadLazy();
  3925. },
  3926. () => {
  3927. item.classList.remove('is-ng-current');
  3928. item.classList.add('is-ng-failed');
  3929. loadLazy();
  3930. }
  3931. );
  3932. };
  3933.  
  3934. const intersectionObserver = initIntersectionObserver(onItemInview);
  3935.  
  3936. initNgDom({intersectionObserver, query, container});
  3937. };
  3938.  
  3939. const init = () => {
  3940. initDom();
  3941. initZenzaBridge();
  3942.  
  3943. const infoView = createVideoInfoView();
  3944. const dispatcher = createCommandDispatcher({infoView});
  3945.  
  3946. infoView.on('command', dispatcher);
  3947.  
  3948. const hoverMenu = new HoverMenu();
  3949. hoverMenu.on('info', (watchId) => {
  3950. hoverMenu.isBusy = true;
  3951.  
  3952. dispatcher('info', watchId)
  3953. .then(() => { hoverMenu.isBusy = false; });
  3954. });
  3955. hoverMenu.on('deflist-add', (watchId, src) => {
  3956. dispatcher('deflist-add', watchId, src);
  3957. });
  3958. hoverMenu.on('deflist-remove', (watchId, src) => {
  3959. dispatcher('deflist-remove', watchId, src);
  3960. });
  3961. hoverMenu.on('playlist-queue', (watchId, src) => {
  3962. dispatcher('playlist-queue', watchId, src);
  3963. });
  3964. MylistPocket.debug.hoverMenu = hoverMenu;
  3965.  
  3966. initNg();
  3967.  
  3968. initExternal(dispatcher, hoverMenu, infoView);
  3969. };
  3970.  
  3971. init();
  3972. };
  3973.  
  3974. const postToParent = function(type, message, token) {
  3975. const origin = document.referrer;
  3976. //console.info('postToParent type=%s, message=%s, token=%s, origin=%s',
  3977. // type, message, token, origin);
  3978. try {
  3979. parent.postMessage(JSON.stringify({
  3980. id: PRODUCT,
  3981. type: type,
  3982. body: {
  3983. token: token,
  3984. url: location.href,
  3985. message: message
  3986. }
  3987. }),
  3988. origin);
  3989. } catch (e) {
  3990. alert(e);
  3991. console.log('err', e);
  3992. }
  3993. };
  3994.  
  3995.  
  3996. const thumbInfoApi = function() {
  3997. if (window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') < 0 ) { return; }
  3998. window.console.log(
  3999. '%cCrossDomainGate: %s %s',
  4000. 'background: lightgreen;',
  4001. PRODUCT,
  4002. location.host);
  4003.  
  4004. const parentHost = document.referrer.split('/')[2];
  4005. if (!parentHost.match(/^[a-z0-9]*.nicovideo.jp$/)) {
  4006. window.console.log('disable bridge');
  4007. return;
  4008. }
  4009.  
  4010. const type = 'thumbInfo' + PRODUCT;
  4011. const token = location.hash ? location.hash.substr(1) : null;
  4012. location.hash = '';
  4013.  
  4014. window.addEventListener('message', (event) => {
  4015. const data = JSON.parse(event.data);
  4016. let timeoutTimer, isTimeout = false;
  4017.  
  4018. if (data.token !== token) { return; }
  4019. //window.console.log('child onMessage', data, event);
  4020.  
  4021.  
  4022. if (!data.url) { return; }
  4023. const sessionId = data.sessionId;
  4024. fetch(data.url).then((resp) => {
  4025. return resp.text();
  4026. }).then((text) => {
  4027. if (isTimeout) { return; }
  4028. else { window.clearTimeout(timeoutTimer); }
  4029.  
  4030. try {
  4031. postToParent(type, {
  4032. sessionId: sessionId,
  4033. status: 'ok',
  4034. token: token,
  4035. url: data.url,
  4036. body: text
  4037. });
  4038. } catch (e) {
  4039. console.log(
  4040. '%cError: parent.postMessage - ',
  4041. 'color: red; background: yellow',
  4042. e, event.origin, event.data);
  4043. }
  4044. });
  4045.  
  4046. timeoutTimer = window.setTimeout(() => {
  4047. isTimeout = true;
  4048. postToParent(type, {
  4049. sessionId: sessionId,
  4050. status: 'timeout',
  4051. command: 'loadUrl',
  4052. url: data.url
  4053. });
  4054. }, 30000);
  4055.  
  4056. });
  4057.  
  4058. try {
  4059. postToParent(type, { status: 'initialized' });
  4060. } catch (e) {
  4061. console.log('err', e);
  4062. }
  4063. };
  4064.  
  4065.  
  4066. const loadGm = function() {
  4067. const script = document.createElement('script');
  4068. script.id = `${PRODUCT}Loader`;
  4069. script.setAttribute('type', 'text/javascript');
  4070. script.setAttribute('charset', 'UTF-8');
  4071. script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' ));
  4072. document.body.appendChild(script);
  4073. };
  4074.  
  4075.  
  4076. const host = window.location.host || '';
  4077. //const href = (location.href || '').replace(/#.*$/, '');
  4078. //const prot = location.protocol;
  4079. if (host === 'ext.nicovideo.jp' &&
  4080. window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') >= 0) {
  4081. thumbInfoApi();
  4082. } else if (window === top) {
  4083. loadGm();
  4084. }
  4085. })();