MylistPocket

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

目前为 2018-05-08 提交的版本。查看 最新版本

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