MylistPocket

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

当前为 2018-04-27 提交的版本,查看 最新版本

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