MylistPocket

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

当前为 2019-08-26 提交的版本,查看 最新版本

  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. // @match https://*.bing.com/*
  21. // @exclude *://ads*.nicovideo.jp/*
  22. // @exclude *://www.upload.nicovideo.jp/*
  23. // @exclude *://www.nicovideo.jp/watch/*?edit=*
  24. // @exclude *://ch.nicovideo.jp/tool/*
  25. // @exclude *://flapi.nicovideo.jp/*
  26. // @exclude *://dic.nicovideo.jp/p/*
  27. // @exclude *://ext.nicovideo.jp/thumb/*
  28. // @exclude *://ext.nicovideo.jp/thumb_channel/*
  29. // @version 0.5.3
  30. // @grant none
  31. // @author segabito macmoto
  32. // @license public domain
  33. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js
  34. // ==/UserScript==
  35. /* eslint-disable */
  36.  
  37. const AntiPrototypeJs = function() {
  38. if (this.promise || !window.Prototype || window.PureArray) {
  39. return this.promise || Promise.resolve(window.PureArray || window.Array);
  40. }
  41. const f = document.createElement('iframe');
  42. f.srcdoc = '<html><title>ここだけ時間が10年遅れてるスレ</title></html>';
  43. Object.assign(f.style, { position: 'absolute', left: '-100vw', top: '-100vh' });
  44. return this.promise = new Promise(res => {
  45. f.onload = res;
  46. document.documentElement.append(f);
  47. }).then(() => {
  48. window.PureArray = f.contentWindow.Array;
  49. delete window.Array.prototype.toJSON;
  50. delete window.Array.prototype.toJSON;
  51. delete window.String.prototype.toJSON;
  52. f.remove();
  53. return Promise.resolve(window.PureArray);
  54. });
  55. }.bind({promise: null});
  56. AntiPrototypeJs().then(() => {
  57. const PRODUCT = 'MylistPocket';
  58.  
  59. const monkey = (PRODUCT) => {
  60. const console = window.console;
  61. const {workerUtil} = window.MylistPocketLib;
  62. //const $ = window.jQuery;
  63. console.log(`%c${PRODUCT}`,
  64. 'font-family: "Apple LiGothic"; padding: 4px; background: red; color: white; font-size: 150%;'
  65. );
  66. const TOKEN = 'r:' + (Math.random());
  67.  
  68. const CONSTANT = {
  69. BASE_Z_INDEX: 100000
  70. };
  71. const MylistPocket = {debug: {}};
  72. window.MylistPocket = MylistPocket;
  73.  
  74. const protocol = location.protocol;
  75.  
  76. const __css__ = (`
  77. a[href*='watch/'] > g-img {
  78. position: inherit;
  79. }
  80.  
  81. .mylistPocketHoverMenu {
  82. display: none;
  83. opacity: 0.8;
  84. position: absolute;
  85. z-index: ${CONSTANT.BASE_Z_INDEX + 100000};
  86. font-size: 8pt;
  87. padding: 0;
  88. line-height: 26px;
  89. font-weight: bold;
  90. text-align: center;
  91. transition: box-shadow 0.2s ease, opacity 0.4s ease, padding 0.2s ease;
  92. user-select: none;
  93. }
  94.  
  95. .mylistPocketHoverMenu.is-busy {
  96. opacity: 0 !important;
  97. pointer-events: none;
  98. }
  99. .mylistPocketHoverMenu.is-otherDomain .wwwOnly {
  100. display: none;
  101. }
  102. .mylistPocketHoverMenu.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
  103. display: none;
  104. }
  105. .mylistPocketHoverMenu .zenzaMenu {
  106. display: none;
  107. }
  108. .mylistPocketHoverMenu.is-zenzaReady .zenzaMenu {
  109. display: inline-block;
  110. }
  111.  
  112.  
  113. .mylistPocketButton {
  114. /*font-family: Menlo;*/
  115. display: block;
  116. font-weight: bolder;
  117. cursor: pointer;
  118. width: 32px;
  119. height: 26px;
  120. background: #ccc;
  121. color: black;
  122. cursor: pointer;
  123. box-shadow: 1px 1px 1px #000;
  124. transition:
  125. 0.1s box-shadow ease,
  126. 0.1s transform ease;
  127. font-size: 16px;
  128. line-height: 24px;
  129. -webkit-user-select: none;
  130. -moz-use-select: none;
  131. user-select: none;
  132. outline: none;
  133. }
  134.  
  135. .mylistPocketButton:hover {
  136. transform: scale(1.2);
  137. box-shadow: 4px 4px 5px #000;
  138. }
  139.  
  140. .mylistPocketButton:active {
  141. transform: scale(1.0);
  142. box-shadow: none;
  143. transition: none;
  144. }
  145.  
  146. .is-deflistUpdating .mylistPocketButton.deflist-add::after,
  147. .is-deflistSuccess .mylistPocketButton.deflist-add::after,
  148. .is-deflistFail .mylistPocketButton.deflist-add::after,
  149. .mylistPocketButton:hover::after, #mylistPocket-poupup [tooltip] {
  150. content: attr(tooltip);
  151. position: absolute;
  152. /*top: 0px;
  153. left: 50%;*/
  154. top: 50%;
  155. right: -8px;
  156. padding: 2px 4px;
  157. white-space: nowrap;
  158. font-size: 12px;
  159. color: #fff;
  160. background: #333;
  161. transform: translate3d(-50%, -120%, 0);
  162. transform: translate3d(100%, -50%, 0);
  163. pointer-events: none;
  164. }
  165.  
  166. .is-deflistUpdating .mylistPocketButton.deflist-add {
  167. cursor: wait;
  168. opacity: 0.9;
  169. transform: scale(1.0);
  170. box-shadow: none;
  171. transition: none;
  172. background: #888;
  173. border-style: inset;
  174. }
  175. .is-deflistSuccess .mylistPocketButton.deflist-add,
  176. .is-deflistFail .mylistPocketButton.deflist-add {
  177. transform: scale(1.0);
  178. box-shadow: none;
  179. transition: none;
  180. }
  181. .is-deflistSuccess .mylistPocketButton.deflist-add::after {
  182. content: attr(data-result);
  183. background: #393;
  184. }
  185. .is-deflistFail .mylistPocketButton.deflist-add::after {
  186. content: attr(data-result);
  187. background: #933;
  188. }
  189. .is-deflistUpdating .mylistPocketButton.deflist-add::after {
  190. content: '更新中';
  191. background: #333;
  192. }
  193.  
  194. .mylistPocketButton + .mylistPocketButton {
  195. margin-top: 4px;
  196. }
  197.  
  198. .mylistPocketHoverMenu:hover {
  199. font-weibht: bolder;
  200. opacity: 1;
  201. }
  202.  
  203. .mylistPocketHoverMenu:active {
  204. }
  205.  
  206. .mylistPocketHoverMenu.is-show {
  207. display: block;
  208. }
  209.  
  210. #mylistPocket-popup {
  211. display: none;
  212. perspective: 800px;
  213. }
  214. #mylistPocket-popup.is-firefox {
  215. /*perspective: none !important;*/
  216. position: fixed;
  217. z-index: 200000;
  218. transform: translate3d(-50%, -50%, 0);
  219. opacity: 0;
  220. transition: 0.3s opacity ease;
  221. top: -9999px; left: -9999px;
  222. }
  223.  
  224. #mylistPocket-popup.show {
  225. display: block;
  226. }
  227. #mylistPocket-popup.is-firefox.show {
  228. top: 50%;
  229. left: 50%;
  230. opacity: 1;
  231. }
  232.  
  233.  
  234. #mylistPocket-popup .owner-icon {
  235. width: 64px;
  236. height: 64px;
  237. transform-origin: center;
  238. transform-origin: center;
  239. transition:
  240. 0.2s transform ease,
  241. 0.2s box-shadow ease
  242. ;
  243. }
  244. #mylistPocket-popup .owner-icon:hover {
  245. }
  246.  
  247. #mylistPocket-popup .description a {
  248. color: #ffff00 !important;
  249. text-decoration: none !important;
  250. font-weight: normal !important;
  251. display: inline-block;
  252. }
  253. #mylistPocket-popup .description a.watch {
  254. position: relative;
  255. display: block;
  256. backface-visibility: hidden;
  257. }
  258.  
  259. #mylistPocket-popup .description a[data-title]:hover::after {
  260. content: attr(data-title);
  261. position: absolute;
  262. top: -16px;
  263. left: 0;
  264. word-break: break-all;
  265. line-height: 12px;
  266. padding: 4px;
  267. font-size: 12px;
  268. color: #333;
  269. background: #ffc;
  270. opacity: 0.8;
  271. user-select: none;
  272. pointer-events: none;
  273. }
  274.  
  275. #mylistPocket-popup .description a:visited {
  276. color: #ffff99 !important;
  277. }
  278. #mylistPocket-popup .description button {
  279. /*font-family: Menlo;*/
  280. font-size: 16px;
  281. font-weight: bolder;
  282. margin: 4px 8px;
  283. padding: 4px 8px;
  284. cursor: pointer;
  285. border-radius: 0;
  286. background: #333;
  287. color: #ccc;
  288. border: solid 2px #ccc;
  289. outline: none;
  290. }
  291. #mylistPocket-popup .description button:hover {
  292. transform: translate(-2px,-2px);
  293. box-shadow: 2px 2px 2px #000;
  294. background: #666;
  295. transition:
  296. 0.2s transform ease,
  297. 0.2s box-shadow ease
  298. ;
  299. }
  300. #mylistPocket-popup .description button:active {
  301. transform: none;
  302. box-shadow: none;
  303. transition: none;
  304. }
  305. #mylistPocket-popup .description button:active::hover {
  306. opacity: 0;
  307. }
  308.  
  309. #mylistPocket-popup .watch {
  310. display: block;
  311. position: relative;
  312. line-height: 60px;
  313. box-sizing: border-box;
  314. padding: 4px 16px;;
  315. min-height: 60px;
  316. width: 280px;
  317. margin: 8px 10px;
  318. background: #444;
  319. border-radius: 4px;
  320. }
  321.  
  322. #mylistPocket-popup .watch:hover {
  323. background: #446;
  324. }
  325.  
  326. #mylistPocket-popup .videoThumbnail {
  327. position: absolute;
  328. right: 16px;
  329. height: 60px;
  330. transform-origin: center;
  331. transition:
  332. 0.2s transform ease,
  333. 0.2s box-shadow ease
  334. ;
  335. }
  336. #mylistPocket-popup .videoThumbnail:hover {
  337. transform: scale(2);
  338. box-shadow: 0 0 8px #888;
  339. transition:
  340. 0.2s transform ease 0.5s,
  341. 0.2s box-shadow ease 0.5s
  342. ;
  343. }
  344.  
  345.  
  346. .zenzaPlayerContainer.is-error #mylistPocket-popup,
  347. .zenzaPlayerContainer.is-loading #mylistPocket-popup,
  348. .zenzaPlayerContainer.error #mylistPocket-popup,
  349. .zenzaPlayerContainer.loading #mylistPocket-popup {
  350. opacity: 0;
  351. pointer-events: none;
  352. }
  353.  
  354. .mylistPocketHoverMenu.is-guest .is-need-login {
  355. display: none !important;
  356. }
  357.  
  358. .xDomainLoaderFrame {
  359. position: fixed;
  360. left: -100%;
  361. top: -100%;
  362. width: 64px;
  363. height: 64px;
  364. opacity: 0;
  365. border: 0;
  366. }
  367.  
  368. body.BaseLayout {
  369. margin-top: 0 !important;
  370. }
  371.  
  372. #siteHeader {
  373. position: sticky;
  374. left: 0 !important;
  375. will-change: transform;
  376. }
  377.  
  378. body.nofix #siteHeader {
  379. position: static;
  380. }
  381.  
  382. .RankingMainContainer-header {
  383. position: sticky;
  384. top: 36px;
  385. z-index: 1000;
  386. background:
  387. linear-gradient(to bottom,
  388. rgba(255, 255, 255, 0),
  389. rgba(255, 255, 255, 0.7),
  390. rgba(255, 255, 255, 1.0),
  391. rgba(255, 255, 255, 0.8),
  392. rgba(232, 232, 255, 0)
  393. );
  394. }
  395. .nofix .RankingMainContainer-header {
  396. top: 0;
  397. }
  398.  
  399. .RankingBaseItem {
  400. border-radius: 0 !important;
  401. box-shadow: none !important;
  402. border: 1px solid silver;
  403. pointer-events: none;
  404. user-select: none;
  405. display: grid;
  406. }
  407. .RankingBaseItem .Card-link {
  408. display: grid;
  409. grid-template-rows: 108px auto;
  410. }
  411. .RankingBaseItem .Card-media {
  412. position: static;
  413. pointer-events: auto;
  414. }
  415. .VideoThumbnail {
  416. border-radius: 0 !important;
  417. }
  418. .RankingBaseItem .Card-title {
  419. pointer-events: auto;
  420. user-select: auto;
  421. height: auto;
  422. max-height: 49px;
  423. -webkit-line-clamp: unset;
  424.  
  425. }
  426. .RankingBaseItem .Card-secondary {
  427. width: 100%;
  428. user-select: none;
  429. pointer-events: none;
  430. align-self: end;
  431. overflow: hidden;
  432.  
  433. }
  434.  
  435. [data-nicoad-grade=gold] .Thumbnail.VideoThumbnail {
  436. background: #f7e01c;
  437. }
  438. [data-nicoad-grade=silver] .Thumbnail.VideoThumbnail {
  439. background: #dfeaec;
  440. }
  441.  
  442. .MatrixRanking-body.GlobalHeader#siteHeader #siteHeaderInner {
  443. width: 1232px;
  444. }
  445.  
  446. .MatrixRanking-body .RankingRowRank {
  447. line-height: 48px;
  448. height: 48px;
  449. pointer-events: none;
  450. user-select: none;
  451. }
  452. .MatrixRanking-body .RankingMatrixVideosRow {
  453. width: ${1232 + 64}px;
  454. margin-left: ${-64}px;
  455. }
  456. .MatrixRanking-body .RankingRowRank {
  457. position: sticky;
  458. left: -8px;
  459. z-index: 100;
  460. transform: none;
  461. padding-right: 16px;
  462. width: 64px;
  463. overflow: visible;
  464. text-align: right;
  465. mix-blend-mode: difference;
  466. text-shadow:
  467. 1px 1px 0 #fff,
  468. 1px -1px 0 #fff,
  469. -1px 1px 0 #fff,
  470. -1px -1px 0 #fff;
  471.  
  472. }
  473.  
  474. `).trim();
  475.  
  476. const nicoadHideCss = `
  477. .nicoadVideoItem {
  478. display: none;
  479. }
  480. .MatrixRankingBannerAd,
  481. .RankingMatrixNicoadsRow, .RankingMainNicoad {
  482. display: none;
  483. }
  484. `.trim();
  485.  
  486. const responsiveCss = `
  487.  
  488. @media screen and (max-width: 1350px) {
  489. .RankingGenreListContainer-categoryHelp {
  490. position: static;
  491. }
  492. .GlobalHeader#siteHeader #siteHeaderInner {
  493. width: 1024px;
  494. }
  495. .MatrixRanking-body .BaseLayout-block {
  496. width: ${1024 + 64 * 2}px;
  497. }
  498. .RankingMainContainer-decorateChunk+.RankingMainContainer-decorateChunk,
  499. .RankingMainContainer-decorateChunk>*+* {
  500. margin-top: 0;
  501. }
  502. .RankingMainContainer {
  503. width: ${1024}px;
  504. }
  505. .MatrixRanking-body .RankingMatrixVideosRow {
  506. width: ${1024 + 64}px;
  507. margin-left: ${-64}px;
  508. }
  509. .RankingMatrixNicoadsRow>*+*,
  510. .RankingMatrixVideosRow>:nth-child(n+3) {
  511. margin-left: 13px;
  512. }
  513. .RankingBaseItem {
  514. width: 160px;
  515. height: 196px;
  516. }
  517. .RankingBaseItem .Card-link {
  518. grid-template-rows: 90px auto;
  519. }
  520. .VideoItem.RankingBaseItem .VideoThumbnail {
  521. border-radius: 3px 3px 0 0;
  522. }
  523.  
  524. [data-nicoad-grade] .Thumbnail.VideoThumbnail .Thumbnail-image {
  525. margin: 3px;
  526. background-size: calc(100% + 6px);
  527. }
  528. [data-nicoad-grade] .Thumbnail.VideoThumbnail:after {
  529. width: 40px;
  530. height: 40px;
  531. background-size: 80px 80px;
  532. }
  533. .Thumbnail.VideoThumbnail .VideoLength {
  534. bottom: 3px;
  535. right: 3px;
  536. }
  537. .VideoThumbnailComment {
  538. transform: scale(0.8333);
  539. }
  540. .RankingBaseItem-meta {
  541. position: static;
  542. padding: 0 4px 8px;
  543. }
  544. .VideoItem.RankingBaseItem .VideoItem-metaCount>.VideoMetaCount {
  545. white-space: nowrap;
  546. }
  547. .RankingMainContainer .ToTopButton {
  548. transform: translateX(calc(100vw / 2 - 100% - 36px));
  549. user-select: none;
  550. }
  551. }
  552. `;
  553.  
  554. const __tpl__ = (`
  555. <div class="mylistPocketHoverMenu scalingUI zen-family">
  556. <button class="mylistPocketButton command deflist-add wwwZenzaOnly is-need-login" data-command="deflist"
  557. tooltip="とりあえずマイリスト">&#x271A;</button>
  558. <button class="mylistPocketButton command info" data-command="info"
  559. tooltip="動画情報を表示">?</button>
  560. <button class="mylistPocketButton command playlist-queue zenzaMenu" data-command="playlist-queue"
  561. tooltip="ZenzaWatchのプレイリストに追加">▶</button>
  562. </div>
  563. </div>
  564.  
  565. <div id="mylistPocket-popup" class="zen-family">
  566. <span slot="video-title">【実況】どんぐりころころの大冒険 Part1(最終回)</span>
  567. <a href="/watch/sm9" slot="watch-link"></a>
  568. <img slot="video-thumbnail" data-type="image">
  569. <a slot="owner-page-link" href="https://www.nicovideo.jp/user/1234" class="owner-page-link target-change" data-type="link" rel="noopener"><img slot="owner-icon" class="owner-icon" src="https://nicovideo.cdn.nimg.jp/web/img/user/thumb/blank_s.jpg" data-type="image"></img></a>
  570.  
  571. <span slot="upload-date" data-type="date">1970/01/01 00:00</span>
  572. <span slot="view-counter" data-type="int">12,345</span>
  573. <span slot="mylist-counter" data-type="int">6,789</span>
  574. <span slot="comment-counter" data-type="int">2,525</span>
  575.  
  576. <span slot="duration" class="duration">1:23</span>
  577.  
  578. <span slot="owner-id">1234</span>
  579. <span slot="locale-owner-name">ほげほげ</span>
  580.  
  581. <div slot="error-description"></div>
  582. <div class="description" slot="description" data-type="html"></div>
  583. <span slot="last-res-body"></span>
  584.  
  585. </div>
  586.  
  587. <template id="mylistPocket-popup-template">
  588. <style>
  589.  
  590. :host(#mylistPocket-popup) {
  591. position: fixed;
  592. z-index: 200000;
  593. transform: translate3d(-50%, -50%, 0);
  594. opacity: 0;
  595. transition: 0.3s opacity ease;
  596. top: -9999px; left: -9999px;
  597. }
  598.  
  599. :host(#mylistPocket-popup.show) {
  600. top: 50%;
  601. left: 50%;
  602. opacity: 1;
  603. pointer-events: auto;
  604. }
  605.  
  606. .root.is-otherDomain .wwwOnly {
  607. display: none;
  608. }
  609. .root.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
  610. display: none;
  611. }
  612.  
  613. * {
  614. box-sizing: border-box;
  615. font-kerning: none;
  616. }
  617.  
  618. a {
  619. color: #ffff00;
  620. font-weight: bold;
  621. display: inline-block;
  622. }
  623.  
  624. a:visited {
  625. color: #ffff99;
  626. }
  627.  
  628. button {
  629. font-size: 14px;
  630. padding: 8px 8px;
  631. cursor: pointer;
  632. border-radius: 0;
  633. margin: 0;
  634. background: #333;
  635. color: #ccc;
  636. border: solid 2px #ccc;
  637. outline: none;
  638. line-height: 20px;
  639. user-select: none;
  640. -webkit-user-select: none;
  641. -moz-user-select: none;
  642. }
  643. button:hover {
  644. transform: translate(-4px,-4px);
  645. box-shadow: 4px 4px 4px #000;
  646. background: #666;
  647. transition:
  648. 0.2s transform ease,
  649. 0.2s box-shadow ease
  650. ;
  651. }
  652.  
  653. button.is-updating {
  654. cursor: wait;
  655. }
  656. button.is-active,
  657. button:active {
  658. transform: none;
  659. box-shadow: none;
  660. transition: none;
  661. }
  662. button.is-active::after,
  663. button:active::after {
  664. opacity: 0;
  665. }
  666.  
  667.  
  668. [tooltip] {
  669. position: relative;
  670. }
  671.  
  672. .is-deflistUpdating .deflist-add::after,
  673. .is-deflistSuccess .deflist-add::after,
  674. .is-deflistFail .deflist-add::after,
  675. [tooltip]:hover::after {
  676. content: attr(tooltip);
  677. position: absolute;
  678. top: 0px;
  679. left: 50%;
  680. padding: 2px 4px;
  681. white-space: nowrap;
  682. font-size: 14px;
  683. color: #fff;
  684. background: #333;
  685. transform: translate3d(-50%, -120%, 0);
  686. pointer-events: none;
  687.  
  688. }
  689.  
  690.  
  691. .root {
  692. text-align: left;
  693. outline-offset: 8px;
  694. border: 12px solid rgba(32, 32, 32, 0);
  695. border-radius: 20px;
  696. padding: 8px 0;
  697. background: rgba(0, 0, 0, 0.7);
  698. color: #ccc;
  699. box-shadow: 0 0 16px #000;
  700. transition:
  701. 0.6s -webkit-clip-path ease,
  702. 0.6s clip-path ease,
  703. 0.5s transform ease;
  704. /*0.4s border-radius ease-out 0.4s,
  705. 0.4s height ease-out 0.4s*/
  706. ;
  707. }
  708.  
  709. .root * {
  710. }
  711.  
  712. .root.show {
  713. opacity: 1;
  714. pointer-events: auto !important;
  715. }
  716.  
  717. .root.is-loading,
  718. .root.is-loading.is-ok,
  719. .root.is-loading.is-fail {
  720. text-align: center;
  721. position: relative;
  722. width: 190px;
  723. height: 190px;
  724. padding: 32px;
  725. opacity: 0.8;
  726. cursor: wait;
  727. border-radius: 100%;
  728. clip-path: circle(100px at center) !important;
  729. transition: none;
  730. outline: none;
  731. transform: none !important;
  732. }
  733. .root.is-firefox {
  734. }
  735. .root.is-loading > * {
  736. pointer-events: none;
  737. }
  738.  
  739. .root.is-setting {
  740. transform: rotateX(180deg);
  741. }
  742.  
  743. .root.is-setting > *:not(.setting-panel) {
  744. pointer-events: none;
  745. z-index: 1;
  746. }
  747.  
  748. .root:not(.is-setting) > .setting-panel {
  749. pointer-events: none;
  750. }
  751.  
  752. .root.is-setting > .setting-panel {
  753. display: block;
  754. opacity: 1;
  755. pointer-events: auto;
  756. }
  757.  
  758. .root.is-loading .loading-inner,
  759. .root.is-loading.is-ok .loading-inner,
  760. .root.is-loading.is-fail .loading-inner {
  761. position: absolute;
  762. top: 50%;
  763. left: 50%;
  764. transform: translate3d(-50%, -50%, 0);
  765. }
  766.  
  767. .loading-inner .spinner {
  768. font-size: 64px;
  769. display: inline-block;
  770. animation-name: spin;
  771. animation-iteration-count: infinite;
  772. animation-duration: 3s;
  773. animation-timing-function: linear;
  774. }
  775.  
  776. @keyframes spin {
  777. 0% { transform: rotate(0deg); }
  778. 100% { transform: rotate(1800deg); }
  779. }
  780.  
  781.  
  782.  
  783. .root.is-ok {
  784. width: 800px;
  785. /*clip-path: circle(800px at center);*/
  786. }
  787.  
  788. .root.is-ok.noclip {
  789. clip-path: none;
  790. }
  791.  
  792. .root.is-fail {
  793. font-size: 120%;
  794. white-space: nowrap;
  795. text-align: center;
  796. padding: 16px;
  797. }
  798.  
  799. .root.is-loading>*:not(.loading-now),
  800. .root.is-loading.is-ok>*:not(.loading-now),
  801. .root.is-loading.is-fail>*:not(.loading-now),
  802. .root.is-fail:not(.is-loading)>*:not(.error-info),
  803. .root.is-ok:not(.is-loading)>*:not(.video-detail):not(.setting-panel) {
  804. display: none !important;
  805. }
  806.  
  807. .root.is-loading>.loading-now,
  808. .root.is-fail>.error-info,
  809. .root.is-ok>.video-detail {
  810. display: block;
  811. }
  812.  
  813. .header {
  814. padding: 8px 8px 8px;
  815. font-size: 12px;
  816. }
  817. .upload-date {
  818. margin-right: 8px;
  819. }
  820. .counter span + span {
  821. margin-left: 8px;
  822. }
  823. .video-title {
  824. font-weight: bolder;
  825. font-size: 22px;
  826. margin-bottom: 4px;
  827. }
  828.  
  829. .close-button {
  830. position: absolute;
  831. right: 0;
  832. top: 0;
  833. transition: 0.2s background ease, 0.2s border-color ease;
  834. cursor: pointer;
  835. width: 48px;
  836. height: 48px;
  837. font-size: 28px;
  838. line-height: 36px;
  839. text-align: center;
  840. user-select: none;
  841. border: 6px solid rgba(80, 80, 80, 0.5);
  842. border-color: transparent;
  843. border-radius: 0 16px 0 0;
  844. }
  845. .close-button:hover {
  846. background: #333;
  847. /*border-color: rgba(0, 0, 0, 0.9);*/
  848. /*transform: translate(-50%, -50%) scale(2.5);*/
  849. }
  850. .close-button:active {
  851. /*transform: translate(-50%, -50%) scale(2) rotate(360deg);*/
  852. box-shadow: none;
  853. transition: none;
  854. }
  855.  
  856. .is-setting .close-button {
  857. display: none;
  858. }
  859.  
  860.  
  861.  
  862.  
  863. .main {
  864. display: flex;
  865. background: rgba(0, 0, 0, 0.2);
  866. box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset;
  867. }
  868.  
  869. .main-left {
  870. width: 360px;
  871. padding: 8px;
  872. z-index: 100;
  873. }
  874. .video-thumbnail-container {
  875. position: relative;
  876. width: 360px;
  877. height: 270px;
  878. background: #000;
  879. /*box-shadow: 2px 2px 4px #000;*/
  880. }
  881. .video-thumbnail-container ::slotted(img) {
  882. width: 360px !important;
  883. height: 270px !important;
  884. object-fit: contain;
  885. }
  886.  
  887. .video-thumbnail-container .duration {
  888. position: absolute;
  889. display: inline-block;
  890. right: 0;
  891. bottom: 0;
  892. font-size: 14px;
  893. background: #000;
  894. color: #fff;
  895. padding: 2px 4px;
  896. }
  897. .video-thumbnail-container:hover .duration {
  898. display: none;
  899. }
  900.  
  901.  
  902. .main-right {
  903. position: relative;
  904. padding: 0;
  905. flex-grow: 1;
  906. font-size: 14px;
  907. }
  908.  
  909. ::slotted(.owner-page-link) {
  910. display: inline-block;
  911. vertical-align: middle;
  912. }
  913.  
  914. .owner-page-link img {
  915. border: 1px solid #333;
  916. border-radius: 3px;
  917. }
  918.  
  919. .video-info {
  920. /*background: rgba(0, 0, 0, 0.2);*/
  921. max-height: 282px;
  922. overflow-x: hidden;
  923. overflow-y: scroll;
  924. overscroll-behavior: contain;
  925. }
  926.  
  927. *::-webkit-scrollbar,
  928. .video-info::-webkit-scrollbar {
  929. background: rgba(34, 34, 34, 0.5);
  930. }
  931.  
  932. *::-webkit-scrollbar-thumb,
  933. .video-info::-webkit-scrollbar-thumb {
  934. border-radius: 0;
  935. background: #666;
  936. }
  937.  
  938. *::-webkit-scrollbar-button,
  939. .video-info::-webkit-scrollbar-button {
  940. background: #666;
  941. display: none;
  942. }
  943.  
  944. *::scrollbar,
  945. .video-info::scrollbar {
  946. background: #222;
  947. }
  948.  
  949. *::scrollbar-thumb,
  950. .video-info::scrollbar-thumb {
  951. border-radius: 0;
  952. background: #666;
  953. }
  954.  
  955. *::scrollbar-button,
  956. .video-info::scrollbar-button {
  957. background: #666;
  958. display: none;
  959. }
  960.  
  961. .scrollable {
  962. overscroll-behavior: contain;
  963. }
  964.  
  965. .owner-info {
  966. margin: 16px;
  967. display: table;
  968. }
  969.  
  970. .owner-info * {
  971. vertical-align: middle;
  972. word-break: break-all;
  973. }
  974.  
  975. .owner-info>* {
  976. display: table-cell !important;
  977. }
  978.  
  979. .owner-name {
  980. display: inline-block;
  981. padding: 8px;
  982. font-size: 18px;
  983. }
  984. .owner-info.is-favorited {
  985. font-weight: bolder;
  986. color: orange;
  987. }
  988.  
  989. .owner-info.is-ng {
  990. color: #888;
  991. text-decoration: line-through;
  992. }
  993.  
  994. .is-channel .owner-name::before {
  995. content: 'CH';
  996. margin: 0 4px;
  997. background: #999;
  998. color: #333;
  999. padding: 2px 4px;
  1000. border: 1px solid;
  1001. }
  1002.  
  1003. .locale-owner-name::after {
  1004. content: ' さん';
  1005. }
  1006.  
  1007. .owner-info .add-ng-button,
  1008. .owner-info .add-fav-button {
  1009. visibility: hidden;
  1010. pointer-events: none;
  1011. }
  1012. .is-ng-enable .owner-info:hover .add-ng-button,
  1013. .is-ng-enable .owner-info:hover .add-fav-button {
  1014. visibility: visible;
  1015. pointer-events: auto;
  1016. }
  1017.  
  1018. .description {
  1019. word-break: break-all;
  1020. line-height: 1.5;
  1021. padding: 0 16px 8px;
  1022. }
  1023.  
  1024. .description:first-letter {
  1025. font-size: 24px;
  1026. }
  1027.  
  1028. .last-res-body {
  1029. margin: 16px 16px 0;
  1030. border: 1px solid #ccc;
  1031. padding: 4px;
  1032. border-radius: 4px;
  1033. word-break: break-all;
  1034. font-size: 12px;
  1035. min-height: 24px;
  1036. }
  1037.  
  1038.  
  1039. .footer {
  1040. padding: 8px;
  1041. backface-visibility: hidden;
  1042. }
  1043.  
  1044. .pocket-button {
  1045. cusror: pointer;
  1046. }
  1047.  
  1048. .pocket-button:active {
  1049. }
  1050.  
  1051.  
  1052. .video-tags {
  1053. display: block;
  1054. }
  1055.  
  1056. .tag-container {
  1057. display: inline-block;
  1058. position: relative;
  1059. padding: 4px 8px;
  1060. border: 1px solid #888;
  1061. border-radius: 4px;
  1062. margin: 0 20px 4px 0;
  1063. }
  1064. .tag-container .tag {
  1065. display: inline-block;
  1066. font-size: 14px;
  1067. color: #ccc;
  1068. text-decoration: none;
  1069. cursor: pointer;
  1070. }
  1071. .tag-container .tag.channel-search {
  1072. margin-left: 8px;
  1073. color: #ccc !important;
  1074. padding: 0 8px;
  1075. }
  1076. .tag-container:hover .tag {
  1077. color: #fff !important;
  1078. }
  1079. .tag-container.is-favorited .tag {
  1080. font-weight: bolder;
  1081. color: orange !important;
  1082. }
  1083. .tag-container.is-ng .tag {
  1084. text-decoration: line-through;
  1085. color: #888 !important;
  1086. }
  1087. .zenzaPlayerContainer .tagItemMenu {
  1088. margin: 0 8px;
  1089. }
  1090.  
  1091.  
  1092. .tag-container .add-ng-button,
  1093. .tag-container .add-fav-button {
  1094. position: absolute !important;
  1095. visibility: hidden;
  1096. pointer-events: none;
  1097. }
  1098. .is-ng-enable .tag-container:hover .add-ng-button,
  1099. .is-ng-enable .tag-container:hover .add-fav-button {
  1100. visibility: visible;
  1101. pointer-events: auto;
  1102. width: 24px;
  1103. height: 24px;
  1104. line-height: 24px;
  1105. font-size: 24px;
  1106. vertical-align: bottom;
  1107. display: inline-block;
  1108. }
  1109. .is-ng-enable .tag-container:hover .add-ng-button {
  1110. right: -16px;
  1111. }
  1112. .is-ng-enable .tag-container:hover .add-fav-button {
  1113. left: -16px;
  1114. }
  1115.  
  1116. .footer-menu {
  1117. position: absolute;
  1118. right: 0px;
  1119. bottom: 0px;
  1120. transform: translate3d(0, 120%, 0);
  1121. opacity: 1;
  1122. transition:
  1123. 0.4s opacity ease 0.4s,
  1124. 0.4s transform ease 0.4s;
  1125. }
  1126.  
  1127. .is-setting .video-detail .footer-menu {
  1128. transform: translate3d(0, 0, 0);
  1129. opacity: 0;
  1130. }
  1131.  
  1132. .footer-menu button {
  1133. min-width: 70px;
  1134. }
  1135.  
  1136. .regular-menu {
  1137. display: inline-block;
  1138. background: rgba(0, 0, 0, 0.7);
  1139. position: relative;
  1140. border-radius: 8px;
  1141. padding: 12px 16px;
  1142. box-shadow: 0 0 16px #000;
  1143. }
  1144.  
  1145. .is-deflistUpdating .deflist-add {
  1146. cursor: wait;
  1147. opacity: 0.9;
  1148. transform: scale(1.0);
  1149. box-shadow: none;
  1150. transition: none;
  1151. }
  1152. .is-deflistSuccess .deflist-add,
  1153. .is-deflistFail .deflist-add {
  1154. transform: scale(1.0);
  1155. box-shadow: none;
  1156. transition: none;
  1157. }
  1158. .is-deflistSuccess .deflist-add::after {
  1159. content: attr(data-result);
  1160. background: #393;
  1161. }
  1162. .is-deflistFail .deflist-add::after {
  1163. content: attr(data-result);
  1164. background: #933;
  1165. }
  1166. .is-deflistUpdating .deflist-add::after {
  1167. content: '更新中';
  1168. background: #333;
  1169. }
  1170.  
  1171. .zenza-menu {
  1172. display: none;
  1173. }
  1174.  
  1175. .is-zenzaReady .zenza-menu {
  1176. display: inline-block;
  1177. background: rgba(0, 0, 0, 0.7);
  1178. margin-left: 32px;
  1179. position: relative;
  1180. border-radius: 8px;
  1181. padding: 12px 16px;
  1182. box-shadow: 0 0 16px #000;
  1183. }
  1184.  
  1185. .is-zenzaReady .zenza-menu::after {
  1186. content: 'ZenzaWatch';
  1187. position: absolute;
  1188. left: 50%;
  1189. bottom: 10px;
  1190. padding: 2px 8px;
  1191. transform: translate(-50%, 100%);
  1192. pointer-events: none;
  1193. font-weith: bolder;
  1194. background: rgba(0, 0, 0, 0.7);
  1195. pointer-events: none;
  1196. border-radius: 4px;
  1197. white-space: nowrap;
  1198. }
  1199.  
  1200. .setting-menu {
  1201. display: inline-block;
  1202. background: rgba(0, 0, 0, 0.7);
  1203. margin-left: 32px;
  1204. position: relative;
  1205. border-radius: 8px;
  1206. padding: 12px 16px;
  1207. box-shadow: 0 0 16px #000;
  1208. }
  1209.  
  1210. .toggle-setting-button {
  1211. font-size: 32px;
  1212. border-radius: 100%;
  1213. border: 12px solid #333;
  1214. cursor: pointer;
  1215. background: rgba(32, 32, 32, 1);
  1216. transition:
  1217. 0.2s transform ease
  1218. ;
  1219. }
  1220.  
  1221. .toggle-setting-button:hover {
  1222. transform: scale(1.2);
  1223. box-shadow: none;
  1224. background: rgba(32, 32, 32, 1);
  1225. background: transparent;
  1226. }
  1227.  
  1228. .toggle-setting-button:active {
  1229. transform: scale(1.0);
  1230. }
  1231.  
  1232. .mylist-comment-link {
  1233. cursor: pointer;
  1234. }
  1235.  
  1236. .setting-panel {
  1237. opacity: 0;
  1238. position: absolute;
  1239. top: 0;
  1240. left: 0;
  1241. width: 100%;
  1242. height: 100%;
  1243. padding: 8px 12px;
  1244. z-index: 10000;
  1245. background: rgba(50, 50, 64, 0.9);
  1246. border-radius: 16px;
  1247. color: #ccc;
  1248. /*-webkit-user-select: none;
  1249. user-select: none;*/
  1250. transform: rotateX(180deg);
  1251. transition: 0.25s opacity ease 0.25s;
  1252. }
  1253. .is-setting .setting-panel {
  1254. transition: 0.25s opacity ease;
  1255. }
  1256. .setting-panel-main {
  1257. width: 100%;
  1258. height: 100%;
  1259. overflow-y: scroll;
  1260. overflow-x: hidden;
  1261. }
  1262.  
  1263. .root:not(.is-setting) .setting-panel .footer-menu {
  1264. transform: translate3d(0, 0, 0);
  1265. opacity: 0;
  1266. }
  1267.  
  1268. .root.is-setting .setting-panel .footer-menu {
  1269. right: -12px;
  1270. bottom: -12px;
  1271. transform: translate3d(0, 120%, 0);
  1272. opacity: 1;
  1273. transition:
  1274. opacity 0.4s ease 0.4s,
  1275. transform 0.4s ease 0.4s;
  1276. }
  1277.  
  1278.  
  1279. .close-setting-menu {
  1280. display: inline-block;
  1281. background: rgba(0, 0, 0, 0.7);
  1282. margin-left: 32px;
  1283. position: relative;
  1284. border-radius: 8px;
  1285. padding: 12px 16px;
  1286. box-shadow: 0 0 16px #000;
  1287. }
  1288.  
  1289. .setting-label {
  1290. display: inline-block;
  1291. line-height: 24px;
  1292. padding: 8px;
  1293. }
  1294.  
  1295. .setting-label:hover {
  1296. text-shadow: 0 0 4px #996;
  1297. }
  1298.  
  1299. .setting-label * {
  1300. cursor: pointer;
  1301. }
  1302.  
  1303. .setting-label input[type=checkbox] {
  1304. transform: scale(2);
  1305. margin: 8px;
  1306. vertical-align: middle;
  1307. }
  1308.  
  1309. .setting-label input + span {
  1310. font-size: 16px;
  1311. }
  1312.  
  1313. .setting-label input:checked + span {
  1314. }
  1315.  
  1316.  
  1317. .setting-fav,
  1318. .setting-ng-textarea,
  1319. .setting-fav-textarea {
  1320. display: none;
  1321. }
  1322.  
  1323. .is-ng-enable .setting-fav {
  1324. display: block;
  1325. }
  1326. .is-ng-enable .setting-ng-textarea,
  1327. .is-ng-enable .setting-fav-textarea {
  1328. display: flex;
  1329. }
  1330.  
  1331. .setting-ng-text-column,
  1332. .setting-fav-text-column {
  1333. flex: 1;
  1334. position: relative;
  1335. padding: 8px;
  1336. }
  1337.  
  1338. .setting-ng-text-column textarea,
  1339. .setting-fav-text-column textarea {
  1340. width: 100%;
  1341. height: 150px;
  1342. background: transparent;
  1343. color: #ccc;
  1344. }
  1345.  
  1346. .setting-ng-label {
  1347. display: none;
  1348. }
  1349.  
  1350. .is-ng-enable .setting-ng-label {
  1351. display: inline-block;
  1352. }
  1353.  
  1354.  
  1355. .add-ng-button,
  1356. .add-fav-button {
  1357. display: none;
  1358. }
  1359.  
  1360. .is-ng-enable .add-ng-button,
  1361. .is-ng-enable .add-fav-button {
  1362. display: inline-block;
  1363. position: relative;
  1364. width: 32px;
  1365. height: 32px;
  1366. line-height: 32px;
  1367. font-size: 28px;
  1368. padding: 0;
  1369. margin: 0;
  1370. /*border-radius: 100%;*/
  1371. border: none;
  1372. text-align: center;
  1373. color: red;
  1374. font-weight: bolder;
  1375. cursor: pointer;
  1376. background: transparent;
  1377. box-shadow: none;
  1378. transition:
  1379. 0.2s transform ease,
  1380. 0.2s text-shadow ease;
  1381. }
  1382. .is-ng-enable .add-fav-button {
  1383. color: orange;
  1384. }
  1385. .is-ng-enable .add-ng-button:hover,
  1386. .is-ng-enable .add-fav-button:hover {
  1387. transform: scale(1.2);
  1388. text-shadow: 2px 2px 4px black;
  1389. }
  1390. .is-ng-enable .add-ng-button:active,
  1391. .is-ng-enable .add-fav-button:active {
  1392. transform: scale(1.0);
  1393. text-shadow: 0 0 2px black;
  1394. }
  1395. .is-ng-enable .add-ng-button:hover::after,
  1396. .is-ng-enable .add-fav-button:hover::after {
  1397. content: 'NG登録';
  1398. position: absolute;
  1399. top: 0;
  1400. left: 50%;
  1401. transform: translate(-50%, -80%);
  1402. font-size: 12px;
  1403. line-height: 12px;
  1404. white-space: nowrap;
  1405. background: rgba(192, 192, 192, 0.8);
  1406. color: #000;
  1407. opacity: 0.9;
  1408. padding: 2px 4px;
  1409. text-shadow: none;
  1410. font-weight: normal;
  1411. pointer-evnets: none !important;
  1412. }
  1413. .is-ng-enable .is-ng .add-ng-button:hover::after,
  1414. .is-ng-enable .is-ng .add-fav-button:hover::after {
  1415. content: 'NG解除';
  1416. }
  1417. .is-ng-enable .add-fav-button:hover::after {
  1418. content: '強調登録';
  1419. }
  1420. .is-ng-enable .is-favorited .add-fav-button:hover::after {
  1421. content: '強調解除';
  1422. }
  1423. .is-ng-enable .add-ng-button:active:hover::after,
  1424. .is-ng-enable .add-fav-button:active:hover::after {
  1425. display: none;
  1426. }
  1427.  
  1428. </style>
  1429. <div class="popup root">
  1430. <div class="loading-now">
  1431. <div class="loading-inner">
  1432. <span class="spinner">&#8987;</span>
  1433. </div>
  1434. </div>
  1435. <div class="error-info">
  1436. <slot name="error-description"></slot>
  1437. </div>
  1438. <div class="video-detail">
  1439. <div class="header">
  1440. <div class="video-title"><slot name="video-title"></slot></div>
  1441.  
  1442. <span class="upload-date">投稿: <slot name="upload-date"/></span>
  1443. <span class="counter">
  1444. <span class="view-counter">再生: <slot name="view-counter"/></span>
  1445. <span class="comment-counter">コメント: <slot name="comment-counter"/></span>
  1446. <span class="mylist-counter command2" data-command="mylist-comment-open">マイリスト:
  1447. <span class="mylist-comment-link command" data-command="mylist-comment-open">&#x274F;</span>
  1448. <slot name="mylist-counter"/>
  1449. </span>
  1450. </span>
  1451. <div class="close-button command" data-command="close" tooltip="閉じる">
  1452. &#x2716;
  1453. </div>
  1454. </div>
  1455.  
  1456. <div class="main">
  1457.  
  1458. <div class=" main-left">
  1459. <div class="video-thumbnail-container">
  1460. <slot name="video-thumbnail"></slot>
  1461. <span class="duration"><slot name="duration"></slot></slot>
  1462. </div>
  1463. </div>
  1464.  
  1465. <div class="video-info main-right scrollable">
  1466.  
  1467. <div class="owner-info">
  1468. <slot name="owner-page-link"></slot>
  1469. <span class="owner-name"><slot name="locale-owner-name"></slot>
  1470. <button class="add-fav-button command" data-command="toggle-fav-owner">★</button>
  1471. <button class="add-ng-button command" data-command="toggle-ng-owner">&#x2716;</button>
  1472. </span>
  1473. </div>
  1474.  
  1475. <div class="description">
  1476. <slot name="description"></slot>
  1477. </div>
  1478.  
  1479. <div class="last-res-body">
  1480. <slot name="last-res-body"></slot>
  1481. </div>
  1482.  
  1483.  
  1484. </div>
  1485.  
  1486. </div>
  1487.  
  1488. <div class="footer">
  1489. <div class="video-tags">
  1490. <slot name="tag"></slot>
  1491. </div>
  1492. </div>
  1493. <div class="footer-menu scalingUI">
  1494. <div class="regular-menu">
  1495. <button
  1496. class="mylistPocketButton deflist-add pocket-button command command-watch-id wwwZenzaOnly"
  1497. data-command="deflist-add"
  1498. tooltip="とりあえずマイリスト"
  1499. >とり</button>
  1500. <button
  1501. class="pocket-button command command-watch-id"
  1502. data-command="mylist-window"
  1503. tooltip="マイリスト"
  1504. >マイ</button>
  1505. <button
  1506. class="pocket-button command command-watch-id"
  1507. data-command="open-mylist-open"
  1508. tooltip="公開マイリスト"
  1509. >公開</button>
  1510. <button
  1511. class="pocket-button command command-video-id"
  1512. data-command="twitter-hash-open"
  1513. tooltip="Twitterの反応"
  1514. >#Twitter</button>
  1515. </div>
  1516.  
  1517.  
  1518. <div class="zenza-menu">
  1519. <button
  1520. class="pocket-button command command-watch-id"
  1521. data-command="zenza-open-now"
  1522. tooltip="ZenzaWatchで開く"
  1523. >Zen</button>
  1524. <button
  1525. class="pocket-button command command-watch-id"
  1526. data-command="playlist-inert"
  1527. tooltip="プレイリスト(次に再生)"
  1528. >playlist</button>
  1529. <button
  1530. class="pocket-button command command-watch-id"
  1531. data-command="playlist-queue"
  1532. tooltip="プレイリスト(末尾に追加)"
  1533. >▶</button>
  1534. </div>
  1535.  
  1536. <div class="setting-menu">
  1537. <button
  1538. class="pocket-button command"
  1539. data-command="toggle-setting"
  1540. >設 定</button>
  1541. </div>
  1542.  
  1543. </div>
  1544. </div>
  1545. <div class="setting-panel">
  1546.  
  1547. <div class="setting-panel-main scrollable">
  1548. <h2>MylistPocket 設定</h2>
  1549. <label class="setting-label">
  1550. <input
  1551. type="checkbox"
  1552. class="setting-form"
  1553. data-config-name="openNewWindow"
  1554. >
  1555. <span>タグやリンクを新しいタブで開く (次回から反映)</span>
  1556. </label>
  1557.  
  1558. <label class="setting-label">
  1559. <input
  1560. type="checkbox"
  1561. class="setting-form"
  1562. data-config-name="enableAutoComment"
  1563. data-config-namespace="mylist"
  1564. >
  1565. <span>マイリストコメントに投稿者名を入れる</span>
  1566. </label>
  1567.  
  1568. <label class="setting-label">
  1569. <input
  1570. type="checkbox"
  1571. class="setting-form"
  1572. data-config-name="responsive.matrix"
  1573. data-config-namespace=""
  1574. >
  1575. <span>ランキングTOPのサムネイルを画面幅に合わせて小さくする</span>
  1576. </label>
  1577.  
  1578. <h2>NG設定(リロード後に反映)</h2>
  1579. <label class="setting-label">
  1580. <input
  1581. type="checkbox"
  1582. class="setting-form"
  1583. data-config-name="enable"
  1584. data-config-namespace="ng"
  1585. >
  1586. <span>簡易NG&強調機能を使う</span>
  1587. </label>
  1588.  
  1589. <label class="setting-label">
  1590. <input
  1591. type="checkbox"
  1592. class="setting-form"
  1593. data-config-name="hide"
  1594. data-config-namespace="nicoad"
  1595. >
  1596. <span>検索結果やランキングのニコニ広告を消す</span>
  1597. </label>
  1598.  
  1599. <label class="setting-label wwwOnly wwwZenzaOnly setting-ng-label">
  1600. <input
  1601. type="checkbox"
  1602. class="setting-form"
  1603. data-config-name="syncZenza"
  1604. data-config-namespace="ng"
  1605. >
  1606. <span>NGタグ・投稿者をZenzaWatchにも反映する</span>
  1607. </label>
  1608.  
  1609. <div class="setting-ng-textarea setting-ng">
  1610. <div class="setting-ng-text-column">
  1611. 投稿者ID
  1612. <textarea
  1613. class="setting-form"
  1614. data-config-name="owner"
  1615. data-config-namespace="ng"
  1616. ></textarea>
  1617. </div>
  1618. <div class="setting-ng-text-column">
  1619. タグ
  1620. <textarea
  1621. class="setting-form"
  1622. data-config-name="tag"
  1623. data-config-namespace="ng"
  1624. ></textarea>
  1625. </div>
  1626. <div class="setting-ng-text-column">
  1627. タイトル・説明文
  1628. <textarea
  1629. class="setting-form"
  1630. data-config-name="word"
  1631. data-config-namespace="ng"
  1632. ></textarea>
  1633. </div>
  1634. </div>
  1635. <h2 class="setting-fav">強調表示設定</h2>
  1636. <div class="setting-fav-textarea setting-fav">
  1637. <div class="setting-fav-text-column">
  1638. 投稿者ID
  1639. <textarea
  1640. class="setting-form"
  1641. data-config-name="owner"
  1642. data-config-namespace="fav"
  1643. ></textarea>
  1644. </div>
  1645. <div class="setting-fav-text-column">
  1646. タグ
  1647. <textarea
  1648. class="setting-form"
  1649. data-config-name="tag"
  1650. data-config-namespace="fav"
  1651. ></textarea>
  1652. </div>
  1653. <div class="setting-fav-text-column">
  1654. タイトル・説明文
  1655. <textarea
  1656. class="setting-form"
  1657. data-config-name="word"
  1658. data-config-namespace="fav"
  1659. ></textarea>
  1660. </div>
  1661. </div>
  1662.  
  1663. </div>
  1664.  
  1665. <div class="footer-menu">
  1666. <div class="close-setting-menu">
  1667. <button
  1668. class="pocket-button command"
  1669. data-command="toggle-setting"
  1670. >戻 る</button>
  1671. </div>
  1672. </div>
  1673.  
  1674. </div>
  1675. </div>
  1676. </template>
  1677. `).trim();
  1678.  
  1679. const __ng_css__ = `
  1680. /* .item_cell 将棋盤ランキング .item 従来のランキングと検索 */
  1681.  
  1682. .RankingMainVideo.is-ng-wait,
  1683. .RankingBaseItem.is-ng-wait,
  1684. .item_cell.is-ng-wait .item,
  1685. .item.is-ng-wait {
  1686. outline: 1px dotted rgba(192, 192, 192, 0.8);
  1687. }
  1688.  
  1689. .RankingMainVideo.is-ng-queue,
  1690. .RankingBaseItem.is-ng-queue,
  1691. .item_cell.is-ng-queue .item,
  1692. .item.is-ng-queue {
  1693. outline: 2px dotted rgba(192, 192, 192, 0.8);
  1694. }
  1695.  
  1696. .RankingMainVideo.is-ng-current,
  1697. .RankingBaseItem.is-ng-current,
  1698. .item_cell.is-ng-current .item,
  1699. .item.is-ng-current {
  1700. outline: 3px dotted rgba(128, 225, 128, 0.8);
  1701. }
  1702.  
  1703. .RankingMainVideo.is-ng-resolved,
  1704. .RankingBaseItem.is-ng-resolved,
  1705. .item_cell.is-ng-resolved .item,
  1706. .item.is-ng-resolved {
  1707. outline: 0px solid green;
  1708. }
  1709.  
  1710. .RankingMainVideo.is-ng-favorited,
  1711. .RankingBaseItem.is-ng-favorited,
  1712. .item_cell.is-fav-favorited .item,
  1713. .item.is-fav-favorited {
  1714. outline: 3px dotted orange;
  1715. outline-offset: 3px;
  1716. }
  1717. .item.videoRanking.is-fav-favorited {
  1718. outline-offset: -3px;
  1719. }
  1720.  
  1721. .RankingBaseItem.is-ng-rejected,
  1722. .item_cell.is-ng-rejected {
  1723. opacity: 0;
  1724. pointer-events: none;
  1725. visibility: hidden;
  1726. }
  1727.  
  1728. .VideoItem .VideoItem-postDate {
  1729. line-height: 16px;
  1730. vertical-align: top;
  1731. font-size: 12px;
  1732. color: #666;
  1733. }
  1734.  
  1735. .RankingMainVideo.is-ng-rejected,
  1736. .item.is-ng-rejected {
  1737. display: none;
  1738. opacity: 0;
  1739. pointer-events: none;
  1740. }
  1741.  
  1742. .NicorepoTimelineItem.is-ng-rejected {
  1743. display: none;
  1744. opacity: 0;
  1745. pointer-events: none;
  1746. }
  1747.  
  1748. body.is-ng-disable .is-ng-rejected {
  1749. outline: none;
  1750. display: block !important;
  1751. pointer-events: auto;
  1752. opacity: 0.5;
  1753. visibility: visible;
  1754. }
  1755.  
  1756. /* チャンネル検索 */
  1757. #search .item.is-ng-rejected {
  1758. display: none;
  1759. }
  1760. `;
  1761.  
  1762. // TODO: ライブラリ化
  1763. const util = MylistPocket.util = (() => {
  1764. const util = {};
  1765.  
  1766. util.mixin = function(self, o) {
  1767. Object.keys(o).forEach(f => {
  1768. if (!_.isFunction(o[f])) { return; }
  1769. if (_.isFunction(self[f])) { return; }
  1770. self[f] = o[f].bind(o);
  1771. });
  1772. };
  1773. util.attachShadowDom = function({host, tpl, mode = 'open'}) {
  1774. const root = host.attachShadow ?
  1775. host.attachShadow({mode}) : host.createShadowRoot();
  1776. const node = document.importNode(tpl.content, true);
  1777. root.appendChild(node);
  1778. return root;
  1779. };
  1780. util.httpLink = function(html) {
  1781. let links = {}, keyCount = 0;
  1782. const getTmpKey = function() { return ` <!--${keyCount++}--> `; };
  1783. html = html.replace(/@([a-zA-Z0-9_]+)/g,
  1784. (g, id) => {
  1785. const tmpKey = getTmpKey();
  1786. links[tmpKey] =
  1787. ` <a href="https://twitter.com/${id}" class="twitterLink" rel="noopener" target="_blank">@${id}</a> `;
  1788. return tmpKey;
  1789. });
  1790.  
  1791.  
  1792. html = html.replace(/(im)(\d+)/g,
  1793. ' <a href="//seiga.nicovideo.jp/seiga/$1$2" class="seigaLink" rel="noopener" target="_blank">$1$2</a> ');
  1794. html = html.replace(/(co)(\d+)/g,
  1795. ' <a href="//com.nicovideo.jp/community/$1$2" class="communityLink" rel="noopener" target="_blank">$1$2</a> ');
  1796. html = html.replace(/(watch|mylist|user)\/(\d+)/g, ' <a href="https://www.nicovideo.jp/$1/$2" rel="noopener" class="videoLink target-change">$1/$2</a> ');
  1797. html = html.replace(/(sm|nm|so)(\d+)/g, ' <a href="https://www.nicovideo.jp/watch/$1$2" rel="noopener" class="videoLink target-change">$1$2</a> ');
  1798.  
  1799. let linkmatch = /<a.*?<\/a>/, n;
  1800. html = html.split('<br />').join(' <br /> ');
  1801. while ((n = linkmatch.exec(html)) !== null) {
  1802. let tmpKey = getTmpKey();
  1803. links[tmpKey] = n;
  1804. html = html.replace(n, tmpKey);
  1805. }
  1806.  
  1807. html = html.replace(/\((https?:\/\/[\x21-\x3b\x3d-\x7e]+)\)/gi, '( $1 )');
  1808. html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)http/gi, '$1 http');
  1809. html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)/gi, '<a href="$1" rel="noopener" target="_blank" class="otherSite">$1</a>');
  1810. Object.keys(links).forEach(tmpKey => {
  1811. html = html.replace(tmpKey, links[tmpKey]);
  1812. });
  1813.  
  1814. html = html.split(' <br /> ').join('<br />');
  1815. return html;
  1816. };
  1817.  
  1818. util.getSleepPromise = function(sleepTime, label = 'sleep') {
  1819. return function(result) {
  1820. return new Promise(resolve => {
  1821. window.setTimeout(() => {
  1822. return resolve(result);
  1823. }, sleepTime);
  1824. });
  1825. };
  1826. };
  1827.  
  1828. util.isFirefox = () =>
  1829. navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
  1830.  
  1831. return util;
  1832. })();
  1833. const css = {
  1834. addStyle: (styles, option, document = window.document) => {
  1835. const elm = document.createElement('style');
  1836. elm.type = 'text/css';
  1837. if (typeof option === 'string') {
  1838. elm.id = option;
  1839. } else if (option) {
  1840. Object.assign(elm, option);
  1841. }
  1842. elm.classList.add(PRODUCT);
  1843. const text = document.createTextNode(styles.toString());
  1844. elm.appendChild(text);
  1845. (document.head || document.body || document.documentElement).append(elm);
  1846. elm.disabled = option && option.disabled;
  1847. elm.dataset.switch = elm.disabled ? 'off' : 'on';
  1848. return elm;
  1849. }
  1850. };
  1851. Object.assign(util, css);
  1852. Object.assign(util, workerUtil);
  1853. const nicoUtil = {
  1854. parseWatchQuery: query => {
  1855. try {
  1856. const result = textUtil.parseQuery(query);
  1857. const playlist = JSON.parse(textUtil.decodeBase64(result.playlist));
  1858. if (playlist.searchQuery) {
  1859. const sq = playlist.searchQuery;
  1860. if (sq.type === 'tag') {
  1861. result.playlist_type = 'tag';
  1862. result.tag = sq.query;
  1863. } else {
  1864. result.playlist_type = 'search';
  1865. result.keyword = sq.query;
  1866. }
  1867. let [order, sort] = (sq.sort || '+f').split('');
  1868. result.order = order === '-' ? 'a' : 'd';
  1869. result.sort = sort;
  1870. if (sq.fRange) { result.f_range = sq.fRange; }
  1871. if (sq.lRange) { result.l_range = sq.lRange; }
  1872. } else if (playlist.mylistId) {
  1873. result.playlist_type = 'mylist';
  1874. result.group_id = playlist.mylistId;
  1875. result.order =
  1876. document.querySelector('select[name="sort"]') ?
  1877. document.querySelector('select[name="sort"]').value : '1';
  1878. } else if (playlist.id && playlist.id.includes('temporary_mylist')) {
  1879. result.playlist_type = 'deflist';
  1880. result.group_id = 'deflist';
  1881. result.order =
  1882. document.querySelector('select[name="sort"]') ?
  1883. document.querySelector('select[name="sort"]').value : '1';
  1884. }
  1885. return result;
  1886. } catch(e) {
  1887. return {};
  1888. }
  1889. },
  1890. hasLargeThumbnail: videoId => {
  1891. const threthold = 16371888;
  1892. const cid = videoId.substr(0, 2);
  1893. const fid = videoId.substr(2) * 1;
  1894. if (cid === 'nm') { return false; }
  1895. if (cid !== 'sm' && fid < 35000000) { return false; }
  1896. if (fid < threthold) {
  1897. return false;
  1898. }
  1899. return true;
  1900. },
  1901. getThumbnailUrlByVideoId: videoId => {
  1902. const videoIdReg = /^[a-z]{2}\d+$/;
  1903. if (!videoIdReg.test(videoId)) {
  1904. return null;
  1905. }
  1906. const fileId = parseInt(videoId.substr(2), 10);
  1907. const large = nicoUtil.hasLargeThumbnail(videoId) ? '.L' : '';
  1908. return fileId >= 35374758 ? // このIDから先は新サーバー(おそらく)
  1909. `https://nicovideo.cdn.nimg.jp/thumbnails/${fileId}/${fileId}.L` :
  1910. `https://tn.smilevideo.jp/smile?i=${fileId}.${large}`;
  1911. },
  1912. getWatchId: url => {
  1913. let m;
  1914. if (url && url.indexOf('nico.ms') >= 0) {
  1915. m = /\/\/nico\.ms\/([a-z0-9]+)/.exec(url);
  1916. } else {
  1917. m = /\/?watch\/([a-z0-9]+)/.exec(url || location.pathname);
  1918. }
  1919. return m ? m[1] : null;
  1920. },
  1921. isPremium: () => {
  1922. const h = document.getElementById('siteHeaderNotification');
  1923. return h && h.classList.contains('siteHeaderPremium');
  1924. },
  1925. isLogin: () => document.getElementsByClassName('siteHeaderLogin').length < 1,
  1926. getPageLanguage: () => {
  1927. try {
  1928. let h = document.getElementsByClassName('html')[0];
  1929. return h.lang || 'ja-JP';
  1930. } catch (e) {
  1931. return 'ja-JP';
  1932. }
  1933. },
  1934. openMylistWindow: watchId => {
  1935. window.open(
  1936. `//www.nicovideo.jp/mylist_add/video/${watchId}`,
  1937. 'nicomylistadd',
  1938. 'width=500, height=400, menubar=no, scrollbars=no');
  1939. },
  1940. openTweetWindow: ({watchId, duration, isChannel, title, videoId}) => {
  1941. const nicomsUrl = `https://nico.ms/${watchId}`;
  1942. const watchUrl = `https://www.nicovideo.jp/watch/${watchId}`;
  1943. title = `${title}(${textUtil.secToTime(duration)})`.replace(/@/g, '@ ');
  1944. const nicoch = isChannel ? ',+nicoch' : '';
  1945. const url =
  1946. 'https://twitter.com/intent/tweet?' +
  1947. 'url=' + encodeURIComponent(nicomsUrl) +
  1948. '&text=' + encodeURIComponent(title) +
  1949. '&hashtags=' + encodeURIComponent(videoId + nicoch) +
  1950. '&original_referer=' + encodeURIComponent(watchUrl) +
  1951. '';
  1952. window.open(url, '_blank', 'width=550, height=480, left=100, top50, personalbar=0, toolbar=0, scrollbars=1, sizable=1', 0);
  1953. },
  1954. isGinzaWatchUrl: url => /^https?:\/\/www\.nicovideo\.jp\/watch\//.test(url || location.href),
  1955. getPlayerVer: () => {
  1956. if (!document.getElementById('js-initial-watch-data')) {
  1957. return 'html5';
  1958. }
  1959. if (document.getElementById('watchAPIDataContainer')) {
  1960. return 'flash';
  1961. }
  1962. return 'unknown';
  1963. },
  1964. isZenzaPlayableVideo: () => {
  1965. try {
  1966. if (nicoUtil.getPlayerVer() === 'html5') {
  1967. return true;
  1968. }
  1969. const watchApiData = JSON.parse(document.querySelector('#watchAPIDataContainer').textContent);
  1970. const flvInfo = textUtil.parseQuery(
  1971. decodeURIComponent(watchApiData.flashvars.flvInfo)
  1972. );
  1973. const dmcInfo = JSON.parse(
  1974. decodeURIComponent(watchApiData.flashvars.dmcInfo || '{}')
  1975. );
  1976. const videoUrl = flvInfo.url ? flvInfo.url : '';
  1977. const isDmc = dmcInfo && dmcInfo.time;
  1978. if (isDmc) {
  1979. return true;
  1980. }
  1981. const isSwf = /\/smile\?s=/.test(videoUrl);
  1982. const isRtmp = (videoUrl.indexOf('rtmp') === 0);
  1983. return (isSwf || isRtmp) ? false : true;
  1984. } catch (e) {
  1985. return false;
  1986. }
  1987. },
  1988. getNicoHistory: window.decodeURIComponent(document.cookie.replace(/^.*(nicohistory[^;+]).*?/, ''))
  1989. };
  1990. Object.assign(util, nicoUtil);
  1991. const textUtil = {
  1992. secToTime: sec => {
  1993. return [
  1994. Math.floor(sec / 60).toString().padStart(2, '0'),
  1995. (Math.floor(sec) % 60).toString().padStart(2, '0')
  1996. ].join(':');
  1997. },
  1998. parseQuery: (query = '') => {
  1999. query = query.startsWith('?') ? query.substr(1) : query;
  2000. const result = {};
  2001. query.split('&').forEach(item => {
  2002. const sp = item.split('=');
  2003. const key = decodeURIComponent(sp[0]);
  2004. const val = decodeURIComponent(sp.slice(1).join('='));
  2005. result[key] = val;
  2006. });
  2007. return result;
  2008. },
  2009. parseUrl: url => {
  2010. url = url || 'https://unknown.example.com/';
  2011. const a = document.createElement('a');
  2012. a.href = url;
  2013. return a;
  2014. },
  2015. decodeBase64: str => {
  2016. try {
  2017. return decodeURIComponent(
  2018. escape(atob(
  2019. str.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(str.length / 4) * 4, '=')
  2020. )));
  2021. } catch(e) {
  2022. return '';
  2023. }
  2024. },
  2025. encodeBase64: str => {
  2026. try {
  2027. return btoa(unescape(encodeURIComponent(str)));
  2028. } catch(e) {
  2029. return '';
  2030. }
  2031. },
  2032. escapeHtml: text => {
  2033. const map = {
  2034. '&': '&amp;',
  2035. '\x27': '&#39;',
  2036. '"': '&quot;',
  2037. '<': '&lt;',
  2038. '>': '&gt;'
  2039. };
  2040. return text.replace(/[&"'<>]/g, char => map[char]);
  2041. },
  2042. unescapeHtml: text => {
  2043. const map = {
  2044. '&amp;': '&',
  2045. '&#39;': '\x27',
  2046. '&quot;': '"',
  2047. '&lt;': '<',
  2048. '&gt;': '>'
  2049. };
  2050. return text.replace(/(&amp;|&#39;|&quot;|&lt;|&gt;)/g, char => map[char]);
  2051. },
  2052. escapeToZenkaku: text => {
  2053. const map = {
  2054. '&': '&',
  2055. '\'': '’',
  2056. '"': '”',
  2057. '<': '<',
  2058. '>': '>'
  2059. };
  2060. return text.replace(/["'<>]/g, char => map[char]);
  2061. },
  2062. escapeRegs: text => {
  2063. const match = /[\\^$.*+?()[\]{}|]/g;
  2064. return text.replace(match, '\\$&');
  2065. },
  2066. convertKansuEi: text => {
  2067. let match = /[〇一二三四五六七八九零壱弐惨伍]/g;
  2068. let map = {
  2069. '〇': '0', '零': '0',
  2070. '一': '1', '壱': '1',
  2071. '二': '2', '弐': '2',
  2072. '三': '3', '惨': '3',
  2073. '四': '4',
  2074. '五': '5', '伍': '5',
  2075. '六': '6',
  2076. '七': '7',
  2077. '八': '8',
  2078. '九': '9',
  2079. };
  2080. text = text.replace(match, char => map[char]);
  2081. text = text.replace(/([1-9]?)[十拾]([0-9]?)/g, (n, a, b) => (a && b) ? `${a}${b}` : (a ? a * 10 : 10 + b * 1));
  2082. return text;
  2083. },
  2084. dateToString: date => {
  2085. if (typeof date === 'string') {
  2086. const origDate = date;
  2087. date = date.replace(/\//g, '-');
  2088. const m = /^(\d+-\d+-\d+) (\d+):(\d+):(\d+)/.exec(date);
  2089. if (m) {
  2090. date = new Date(m[1]);
  2091. date.setHours(m[2]);
  2092. date.setMinutes(m[3]);
  2093. date.setSeconds(m[4]);
  2094. } else {
  2095. const t = Date.parse(date);
  2096. if (isNaN(t)) {
  2097. return origDate;
  2098. }
  2099. date = new Date(t);
  2100. }
  2101. } else if (typeof date === 'number') {
  2102. date = new Date(date);
  2103. }
  2104. if (!date || isNaN(date.getTime())) {
  2105. return '1970/01/01 00:00:00';
  2106. }
  2107. const [yy, mm, dd, h, m, s] = [
  2108. date.getFullYear(),
  2109. date.getMonth() + 1,
  2110. date.getDate(),
  2111. date.getHours(),
  2112. date.getMinutes(),
  2113. date.getSeconds()
  2114. ].map(n => n.toString().padStart(2, '0'));
  2115. return `${yy}/${mm}/${dd} ${h}:${m}:${s}`;
  2116. },
  2117. isValidJson: data => {
  2118. try {
  2119. JSON.parse(data);
  2120. return true;
  2121. } catch (e) {
  2122. return false;
  2123. }
  2124. },
  2125. toRgba: (c, alpha = 1) =>
  2126. `rgba(${parseInt(c.substr(1, 2), 16)}, ${parseInt(c.substr(3, 2), 16)}, ${parseInt(c.substr(5, 2), 16)}, ${alpha})`,
  2127. snakeToCamel: snake => snake.replace(/-./g, s => s.charAt(1).toUpperCase()),
  2128. camelToSnake: (camel, separator = '_') => camel.replace(/([A-Z])/g, s => separator + s.toLowerCase())
  2129. };
  2130. Object.assign(util, textUtil);
  2131. const reg = (() => {
  2132. const $ = Symbol('$');
  2133. const undef = Symbol.for('undefined');
  2134. const MAX_RESULT = 30;
  2135. const smap = new WeakMap();
  2136. const self = {};
  2137. const reg = function(regex = undef, str = undef) {
  2138. const {results, last} = smap.has(this) ?
  2139. smap.get(this) : {results: [], last: {result: null}};
  2140. smap.set(this, {results, last});
  2141. if (regex === undef) {
  2142. return last ? last.result : null;
  2143. }
  2144. const regstr = regex.toString();
  2145. if (str !== undef) {
  2146. const found = results.find(r => regstr === r.regstr && str === r.str);
  2147. return found ? found.result : reg(regex).exec(str);
  2148. }
  2149. return {
  2150. exec(str) {
  2151. const result = regex.exec(str);
  2152. Array.isArray(result) && result.forEach((r, i) => result['$' + i] = r);
  2153. Object.assign(last, {str, regstr, result});
  2154. results.push(last);
  2155. results.length > MAX_RESULT && results.shift();
  2156. this[$] = str[$] = regex[$] = result;
  2157. return result;
  2158. },
  2159. test(str) { return !!this.exec(str); }
  2160. };
  2161. };
  2162. const scope = (scopeObj = {}) => reg.bind(scopeObj);
  2163. return Object.assign(reg.bind(self), {$, scope});
  2164. })();
  2165.  
  2166. MylistPocket.emitter = util.emitter = new Emitter();
  2167.  
  2168. const ZenzaDetector = (function() {
  2169. let isReady = false;
  2170. let Zenza = null;
  2171. const emitter = new Emitter();
  2172.  
  2173. const initialize = function() {
  2174. const onZenzaReady = () => {
  2175. isReady = true;
  2176. Zenza = window.ZenzaWatch;
  2177.  
  2178. Zenza.emitter.on('hideHover', () => {
  2179. util.emitter.emit('hideHover');
  2180. });
  2181.  
  2182. Zenza.emitter.on('csrfToken', (token) => {
  2183. util.emitter.emit('csrfToken', token);
  2184. });
  2185.  
  2186. let popup = document.getElementById('mylistPocket-popup');
  2187. let defaultContainer = document.getElementById('mylistPocketDomContainer');
  2188. defaultContainer.classList.add('zen-family');
  2189. let zenzaContainer;
  2190. Zenza.emitter.on('fullScreenStatusChange', isFull => {
  2191. if (isFull) {
  2192. if (!zenzaContainer) {
  2193. zenzaContainer = document.querySelector('.zenzaPlayerContainer');
  2194. }
  2195. zenzaContainer.appendChild(popup);
  2196. } else {
  2197. defaultContainer.appendChild(popup);
  2198. }
  2199. });
  2200. emitter.emit('ready', Zenza);
  2201. };
  2202.  
  2203. if (window.ZenzaWatch && window.ZenzaWatch.ready) {
  2204. window.console.log('ZenzaWatch is Ready');
  2205. onZenzaReady();
  2206. } else {
  2207. document.body.addEventListener('ZenzaWatchInitialize', function() {
  2208. window.console.log('ZenzaWatchInitialize MylistPocket');
  2209. onZenzaReady();
  2210. });
  2211. }
  2212. };
  2213.  
  2214. const detect = function() {
  2215. return new Promise(res => {
  2216. if (isReady) {
  2217. return res(Zenza);
  2218. }
  2219. emitter.on('ready', () => {
  2220. res(Zenza);
  2221. });
  2222. });
  2223. };
  2224.  
  2225. return {
  2226. initialize: initialize,
  2227. detect: detect
  2228. };
  2229.  
  2230. })();
  2231. const objUtil = (() => {
  2232. const isObject = e => e !== null && e instanceof Object;
  2233. return {
  2234. bridge: (self, target, keys = null) => {
  2235. (keys || Object.getOwnPropertyNames(target.constructor.prototype))
  2236. .filter(key => typeof target[key] === 'function')
  2237. .forEach(key => self[key] = target[key].bind(target));
  2238. },
  2239. isObject,
  2240. toMap: (obj, mapper = Map) => {
  2241. if (obj instanceof mapper) {
  2242. return obj;
  2243. }
  2244. const map = new mapper();
  2245. for(const key of Object.keys(map)) {
  2246. map.set(key, obj[key]);
  2247. }
  2248. return map;
  2249. },
  2250. mapToObj: map => {
  2251. if (!(obj instanceof Map)) {
  2252. return map;
  2253. }
  2254. const obj = {};
  2255. for (const [key, val] of map) {
  2256. obj[key] = val;
  2257. }
  2258. return obj;
  2259. }
  2260. };
  2261. })();
  2262. const StorageWriter = (() => {
  2263. const func = function(self) {
  2264. self.onmessage = ({command, params}) => {
  2265. const {obj, replacer, space} = params;
  2266. return JSON.stringify(obj, replacer || null, space || 0);
  2267. };
  2268. };
  2269. let worker;
  2270. const prototypePollution = window.Prototype && Array.prototype.hasOwnProperty('toJSON');
  2271. const toJson = async (obj, replacer = null, space = 0) => {
  2272. if (!prototypePollution || obj === null || ['string', 'number', 'boolean'].includes(typeof obj)) {
  2273. return JSON.stringify(obj, replacer, space);
  2274. }
  2275. worker = worker || workerUtil.createCrossMessageWorker(func, {name: 'ToJsonWorker'});
  2276. return worker.post({command: 'toJson', params: {obj, replacer, space}});
  2277. };
  2278. const writer = Symbol('StorageWriter');
  2279. const setItem = (storage, key, value) => {
  2280. if (!prototypePollution || value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
  2281. storage.setItem(key, JSON.stringify(value));
  2282. } else {
  2283. toJson(value).then(json => storage.setItem(key, json));
  2284. }
  2285. };
  2286. localStorage[writer] = (key, value) => setItem(localStorage, key, value);
  2287. sessionStorage[writer] = (key, value) => setItem(sessionStorage, key, value);
  2288. return { writer, toJson };
  2289. })();
  2290. const Observable = (() => {
  2291. const observableSymbol = Symbol.observable || Symbol('observable');
  2292. const nop = Handler.nop;
  2293. class Subscription {
  2294. constructor({observable, subscriber, unsubscribe, closed}) {
  2295. this.callbacks = {unsubscribe, closed};
  2296. this.observable = observable;
  2297. const next = subscriber.next.bind(subscriber);
  2298. subscriber.next = args => {
  2299. if (this.closed || (this._filterFunc && !this._filterFunc(args))) {
  2300. return;
  2301. }
  2302. return this._mapFunc ? next(this._mapFunc(args)) : next(args);
  2303. };
  2304. this._closed = false;
  2305. }
  2306. subscribe(subscriber, onError, onCompleted) {
  2307. return this.observable.subscribe(subscriber, onError, onCompleted)
  2308. .filter(this._filterFunc)
  2309. .map(this._mapFunc);
  2310. }
  2311. unsubscribe() {
  2312. this._closed = true;
  2313. if (this.callbacks.unsubscribe) {
  2314. this.callbacks.unsubscribe();
  2315. }
  2316. return this;
  2317. }
  2318. dispose() {
  2319. return this.unsubscribe();
  2320. }
  2321. filter(func) {
  2322. const _func = this._filterFunc;
  2323. this._filterFunc = _func ? func : arg => func(_func(arg));
  2324. return this;
  2325. }
  2326. map(func) {
  2327. const _func = this._mapFunc;
  2328. this._mapFunc = _func ? func : arg => func(_func(arg));
  2329. return this;
  2330. }
  2331. get closed() {
  2332. if (this.callbacks.closed) {
  2333. return this._closed || this.callbacks.closed();
  2334. } else {
  2335. return this._closed;
  2336. }
  2337. }
  2338. }
  2339. class Subscriber {
  2340. static create(onNext = null, onError = null, onCompleted = null) {
  2341. if (typeof onNext === 'function') {
  2342. return new this({
  2343. next: onNext,
  2344. error: onError,
  2345. complete: onCompleted
  2346. });
  2347. }
  2348. return new this(onNext || {});
  2349. }
  2350. constructor({start, next, error, complete} = {start:nop, next:nop, error:nop, complete:nop}) {
  2351. this.callbacks = {start, next, error, complete};
  2352. }
  2353. start(arg) {this.callbacks.start(arg);}
  2354. next(arg) {this.callbacks.next(arg);}
  2355. error(arg) {this.callbacks.error(arg);}
  2356. complete(arg) {this.callbacks.complete(arg);}
  2357. get closed() {
  2358. return this._callbacks.closed ? this._callbacks.closed() : false;
  2359. }
  2360. }
  2361. Subscriber.nop = {start: nop, next: nop, error: nop, complete: nop, closed: nop};
  2362. const eleMap = new WeakMap();
  2363. class Observable {
  2364. static of(...args) {
  2365. return new this(o => {
  2366. for (const arg of args) {
  2367. o.next(arg);
  2368. }
  2369. o.complete();
  2370. return () => {};
  2371. });
  2372. }
  2373. static from(arg) {
  2374. if (arg[Symbol.iterator]) {
  2375. return this.of(...arg);
  2376. } else if (arg[Observable.observavle]) {
  2377. return arg[Observable.observavle]();
  2378. }
  2379. }
  2380. static fromEvent(element, eventName) {
  2381. const em = eleMap.get(element) || {};
  2382. if (em && em[eventName]) {
  2383. return em[eventName];
  2384. }
  2385. eleMap.set(element, em);
  2386. return em[eventName] = new this(o => {
  2387. const onUpdate = e => o.next(e);
  2388. element.addEventListener(eventName, onUpdate, {passive: true});
  2389. return () => element.removeEventListener(eventName, onUpdate);
  2390. });
  2391. }
  2392. static interval(ms) {
  2393. return new this(function(o) {
  2394. const timer = setInterval(() => o.next(this.i++), ms);
  2395. return () => clearInterval(timer);
  2396. }.bind({i: 0}));
  2397. }
  2398. constructor(subscriberFunction) {
  2399. this._subscriberFunction = subscriberFunction;
  2400. this._completed = false;
  2401. this._cancelled = false;
  2402. this._handlers = new Handler();
  2403. }
  2404. _initSubscriber() {
  2405. if (this._subscriber) {
  2406. return;
  2407. }
  2408. const handlers = this._handlers;
  2409. this._completed = this._cancelled = false;
  2410. return this._subscriber = new Subscriber({
  2411. start: arg => handlers.execMethod('start', arg),
  2412. next: arg => handlers.execMethod('next', arg),
  2413. error: arg => handlers.execMethod('error', arg),
  2414. complete: arg => {
  2415. if (this._nextObservable) {
  2416. this._nextObservable.subscribe(this._subscriber);
  2417. this._nextObservable = this._nextObservable._nextObservable;
  2418. } else {
  2419. this._completed = true;
  2420. handlers.execMethod('complete', arg);
  2421. }
  2422. },
  2423. closed: () => this.closed
  2424. });
  2425. }
  2426. get closed() {
  2427. return this._completed || this._cancelled;
  2428. }
  2429. filter(func) {
  2430. return this.subscribe().filter(func);
  2431. }
  2432. map(func) {
  2433. return this.subscribe().map(func);
  2434. }
  2435. concat(arg) {
  2436. const observable = Observable.from(arg);
  2437. if (this._nextObservable) {
  2438. this._nextObservable.concat(observable);
  2439. } else {
  2440. this._nextObservable = observable;
  2441. }
  2442. return this;
  2443. }
  2444. forEach(callback) {
  2445. let p = new PromiseHandler();
  2446. callback(p);
  2447. return this.subscribe({
  2448. next: arg => {
  2449. p.resolve(arg);
  2450. p = new PromiseHandler();
  2451. callback(p);
  2452. },
  2453. error: arg => {
  2454. p.reject(arg);
  2455. p = new PromiseHandler();
  2456. callback(p);
  2457. }});
  2458. }
  2459. onStart(arg) { this._subscriber.start(arg); }
  2460. onNext(arg) { this._subscriber.next(arg); }
  2461. onError(arg) { this._subscriber.error(arg); }
  2462. onComplete(arg) { this._subscriber.complete(arg);}
  2463. disconnect() {
  2464. if (!this._disconnectFunction) {
  2465. return;
  2466. }
  2467. this._closed = true;
  2468. this._disconnectFunction();
  2469. delete this._disconnectFunction;
  2470. this._subscriber;
  2471. this._handlers.clear();
  2472. }
  2473. [observableSymbol]() {
  2474. return this;
  2475. }
  2476. subscribe(onNext = null, onError = null, onCompleted = null) {
  2477. this._initSubscriber();
  2478. const isNop = [onNext, onError, onCompleted].every(f => f === null);
  2479. const subscriber = Subscriber.create(onNext, onError, onCompleted);
  2480. return this._subscribe({subscriber, isNop});
  2481. }
  2482. _subscribe({subscriber, isNop}) {
  2483. if (!isNop && !this._disconnectFunction) {
  2484. this._disconnectFunction = this._subscriberFunction(this._subscriber);
  2485. }
  2486. !isNop && this._handlers.add(subscriber);
  2487. return new Subscription({
  2488. observable: this,
  2489. subscriber,
  2490. unsubscribe: () => {
  2491. if (isNop) { return; }
  2492. this._handlers.remove(subscriber);
  2493. if (this._handlers.isEmpty) {
  2494. this.disconnect();
  2495. }
  2496. },
  2497. closed: () => this.closed
  2498. });
  2499. }
  2500. }
  2501. Observable.observavle = observableSymbol;
  2502. return Observable;
  2503. })();
  2504. const WindowResizeObserver = Observable.fromEvent(window, 'resize')
  2505. .map(o => { return {width: window.innerWidth, height: window.innerHeight}; });
  2506. const bounce = {
  2507. origin: Symbol('origin'),
  2508. raf(func) {
  2509. let reqId = null;
  2510. let lastArgs = null;
  2511. const callback = () => {
  2512. func(...lastArgs);
  2513. reqId = lastArgs = null;
  2514. };
  2515. const result = (...args) => {
  2516. if (reqId) {
  2517. cancelAnimationFrame(reqId);
  2518. }
  2519. lastArgs = args;
  2520. reqId = requestAnimationFrame(callback);
  2521. };
  2522. result[this.origin] = func;
  2523. return result;
  2524. },
  2525. idle(func, time) {
  2526. let reqId = null;
  2527. let lastArgs = null;
  2528. const [caller, canceller] =
  2529. (time === undefined && window.requestIdleCallback) ?
  2530. [window.requestIdleCallback, window.cancelIdleCallback] : [window.setTimeout, window.clearTimeout];
  2531. const callback = () => {
  2532. reqId = null;
  2533. func(...lastArgs);
  2534. lastArgs = null;
  2535. };
  2536. const result = (...args) => {
  2537. if (reqId) {
  2538. reqId = canceller(reqId);
  2539. }
  2540. lastArgs = args;
  2541. reqId = caller(callback, time);
  2542. };
  2543. result[this.origin] = func;
  2544. return result;
  2545. },
  2546. time(func, time = 0) {
  2547. return this.idle(func, time);
  2548. }
  2549. };
  2550. class DataStorage {
  2551. static create(defaultData, options = {}) {
  2552. return new DataStorage(defaultData, options);
  2553. }
  2554. constructor(defaultData, options = {}) {
  2555. this._default = defaultData;
  2556. this._data = Object.assign({}, defaultData);
  2557. this.prefix = `${options.prefix || 'DATA'}_`;
  2558. this.storage = options.storage || localStorage;
  2559. this._ignoreExportKeys = options.ignoreExportKeys || [];
  2560. this.readonly = options.readonly;
  2561. this.silently = false;
  2562. this._changed = new Map();
  2563. this._onChange = bounce.idle(this._onChange.bind(this));
  2564. objUtil.bridge(this, new Emitter());
  2565. this.restore();
  2566. this.props = this._makeProps(defaultData);
  2567. }
  2568. _makeProps(defaultData = {}, namespace = '') {
  2569. namespace = namespace ? `${namespace}.` : '';
  2570. const self = this;
  2571. const def = {};
  2572. const props = {};
  2573. Object.keys(defaultData).sort()
  2574. .filter(key => key.includes(namespace))
  2575. .forEach(key => {
  2576. const k = key.slice(namespace.length);
  2577. if (k.includes('.')) {
  2578. const ns = k.slice(0, k.indexOf('.'));
  2579. props[ns] = this._makeProps(defaultData, `${namespace}${ns}`);
  2580. }
  2581. def[k] = {
  2582. enumerable: !this._ignoreExportKeys.includes(key),
  2583. get() { return self.getValue(key); },
  2584. set(v) { self.setValue(key, v); }
  2585. };
  2586. });
  2587. Object.defineProperties(props, def);
  2588. return props;
  2589. }
  2590. _onChange() {
  2591. const changed = this._changed;
  2592. this.emit('change', changed);
  2593. for (const [key, val] of changed) {
  2594. this.emitAsync('update', key, val);
  2595. this.emitAsync(`update-${key}`, val);
  2596. }
  2597. this._changed.clear();
  2598. }
  2599. onkey(key, callback) {
  2600. this.on(`update-${key}`, callback);
  2601. }
  2602. offkey(key, callback) {
  2603. this.off(`update-${key}`, callback);
  2604. }
  2605. restore(storage) {
  2606. storage = storage || this.storage;
  2607. Object.keys(this._default).forEach(key => {
  2608. const storageKey = this.getStorageKey(key);
  2609. if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
  2610. try {
  2611. this._data[key] = JSON.parse(storage[storageKey]);
  2612. } catch (e) {
  2613. window.console.error('config parse error key:"%s" value:"%s" ', key, storage[storageKey], e);
  2614. }
  2615. } else {
  2616. this._data[key] = this._default[key];
  2617. }
  2618. });
  2619. }
  2620. getNativeKey(key) {
  2621. return key;
  2622. }
  2623. getStorageKey(key) {
  2624. return `${this.prefix}${key}`;
  2625. }
  2626. refresh(key, storage) {
  2627. storage = storage || this.storage;
  2628. key = this.getNativeKey(key);
  2629. const storageKey = this.getStorageKey(key);
  2630. if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
  2631. try {
  2632. this._data[key] = JSON.parse(storage[storageKey]);
  2633. } catch (e) {
  2634. window.console.error('config parse error key:"%s" value:"%s" ', key, storage[storageKey], e);
  2635. }
  2636. }
  2637. return this._data[key];
  2638. }
  2639. getValue(key, refresh) {
  2640. if (refresh) {
  2641. return this.refresh(key);
  2642. }
  2643. key = this.getNativeKey(key);
  2644. return this._data[key];
  2645. }
  2646. setValue(key, value) {
  2647. const _key = key;
  2648. key = this.getNativeKey(key);
  2649. if (this._data[key] === value || arguments.length < 2) {
  2650. return;
  2651. }
  2652. const storageKey = this.getStorageKey(key);
  2653. const storage = this.storage;
  2654. if (!this.readonly && storage[StorageWriter.writer]) {
  2655. storage[StorageWriter.writer](storageKey, value);
  2656. } else if (!this.readonly) {
  2657. try {
  2658. storage[storageKey] = JSON.stringify(value);
  2659. } catch (e) {
  2660. window.console.error(e);
  2661. }
  2662. }
  2663. this._data[key] = value;
  2664. if (!this.silently) {
  2665. this._changed.set(_key, value);
  2666. this._onChange();
  2667. }
  2668. }
  2669. setValueSilently(key, value) {
  2670. const isSilent = this.silently;
  2671. this.silently = true;
  2672. this.setValue(key, value);
  2673. this.silently = isSilent;
  2674. }
  2675. export(isAll = false) {
  2676. const result = {};
  2677. const _default = this._default;
  2678. Object.keys(this.props)
  2679. .filter(key => isAll || (_default[key] !== this._data[key]))
  2680. .forEach(key => result[key] = this.getValue(key));
  2681. return result;
  2682. }
  2683. exportJson() {
  2684. return JSON.stringify(this.export(), null, 2);
  2685. }
  2686. import(data) {
  2687. Object.keys(this.props)
  2688. .forEach(key => {
  2689. console.log('import data: %s=%s', key, data[key]);
  2690. this.setValueSilently(key, data[key]);
  2691. });
  2692. }
  2693. importJson(json) {
  2694. this.import(JSON.parse(json));
  2695. }
  2696. getKeys() {
  2697. return Object.keys(this.props);
  2698. }
  2699. clear() {
  2700. this.silently = true;
  2701. const storage = this.storage;
  2702. Object.keys(this._default)
  2703. .filter(key => !this._ignoreExportKeys.includes(key)).forEach(key => {
  2704. const storageKey = this.getStorageKey(key);
  2705. try {
  2706. if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
  2707. delete storage[storageKey];
  2708. }
  2709. this._data[key] = this._default[key];
  2710. } catch (e) {}
  2711. });
  2712. this.silently = false;
  2713. }
  2714. namespace(name) {
  2715. const namespace = name ? `${name}.` : '';
  2716. const origin = Symbol(`${namespace}`);
  2717. const result = {
  2718. getValue: key => this.getValue(`${namespace}${key}`),
  2719. setValue: (key, value) => this.setValue(`${namespace}${key}`, value),
  2720. on: (key, func) => {
  2721. if (key === 'update') {
  2722. const onUpdate = (key, value) => {
  2723. if (key.startsWith(namespace)) {
  2724. func(key.slice(namespace.length + 1), value);
  2725. }
  2726. };
  2727. onUpdate[origin] = func;
  2728. this.on('update', onUpdate);
  2729. return result;
  2730. }
  2731. return this.onkey(`${namespace}${key}`, func);
  2732. },
  2733. off: (key, func) => {
  2734. if (key === 'update') {
  2735. func = func[origin] || func;
  2736. this.off('update', func);
  2737. return result;
  2738. }
  2739. return this.offkey(`${namespace}${key}`, func);
  2740. },
  2741. onkey: (key, func) => {
  2742. this.on(`update-${namespace}${key}`, func);
  2743. return result;
  2744. },
  2745. offkey: (key, func) => {
  2746. this.off(`update-${namespace}${key}`, func);
  2747. return result;
  2748. },
  2749. props: this.props[name],
  2750. refresh: () => this.refresh(),
  2751. subscribe: subscriber => {
  2752. return this.subscribe(subscriber)
  2753. .filter(changed => changed.keys().some(k => k.startsWith(namespace)))
  2754. .map(changed => {
  2755. const result = new Map;
  2756. for (const k of changed.keys()) {
  2757. k.startsWith(namespace) && result.set(k, changed.get(k));
  2758. }
  2759. return result;
  2760. });
  2761. }
  2762. };
  2763. return result;
  2764. }
  2765. subscribe(subscriber) {
  2766. subscriber = subscriber || {
  2767. next: (...args) => window.console.log('next', ...args),
  2768. error: (...args) => window.console.warn('error', ...args),
  2769. complete: (...args) => window.console.log('complete', ...args)
  2770. };
  2771. const observable = new Observable(o => {
  2772. const onChange = changed => o.next(changed);
  2773. this.on('change', onChange);
  2774. return () => this.off(onChange);
  2775. });
  2776. return observable.subscribe(subscriber);
  2777. }
  2778. }
  2779. // already required /Users/kunio/Dropbox/github/ZenzaWatch/packages/lib/src/infra/bounce.js
  2780.  
  2781. const config = (() => {
  2782. const DEFAULT_CONFIG = {
  2783. debug: false,
  2784.  
  2785. 'videoInfo.openNewWindow': false,
  2786. 'mylist.enableAutoComment': true, // マイリストコメントに投稿者を入れる
  2787.  
  2788. 'responsive.matrix': false,
  2789.  
  2790. 'nicoad.hide': false,
  2791.  
  2792. 'ng.enable': false,
  2793. 'ng.owner': '',
  2794. 'ng.word': '',
  2795. 'ng.tag': '',
  2796. 'ng.syncZenza': false,
  2797.  
  2798. 'fav.owner': '',
  2799. 'fav.word': '',
  2800. 'fav.tag': ''
  2801. };
  2802. return new DataStorage(
  2803. DEFAULT_CONFIG,
  2804. {
  2805. prefix: `${PRODUCT}_config`,
  2806. ignoreExportKeys: [],
  2807. readonly: !location || location.host !== 'www.nicovideo.jp',
  2808. storage: localStorage
  2809. }
  2810. );
  2811. })();
  2812.  
  2813. MylistPocket.broadcast = (function(config) {
  2814. if (!window.BroadcastChannel) { return; }
  2815. const broadcastChannel = new window.BroadcastChannel(PRODUCT);
  2816.  
  2817. const onBroadcastMessage = (e) => {
  2818. const data = e.data;
  2819. switch (data.type) {
  2820. case 'config-update':
  2821. config.refresh(true);
  2822. break;
  2823. }
  2824. };
  2825.  
  2826. broadcastChannel.addEventListener('message', onBroadcastMessage);
  2827.  
  2828. return {
  2829. postMessage: (...args) => { broadcastChannel.postMessage(...args); }
  2830. };
  2831.  
  2832. })(config);
  2833. config.on('update', (key, value) => {
  2834. if (!config.props.hasOwnProperty(key)) { return; }
  2835. MylistPocket.broadcast.postMessage(
  2836. {type: 'config-update', key, value, storage: 'local'}
  2837. );
  2838. });
  2839.  
  2840. MylistPocket.config = config;
  2841.  
  2842.  
  2843.  
  2844. const CacheStorage = (function() {
  2845. let PREFIX = PRODUCT + '_cache_';
  2846.  
  2847. class CacheStorage {
  2848.  
  2849. constructor(storage, gc = false) {
  2850. this._storage = storage;
  2851. this._memory = {};
  2852. if (gc) { this.gc(); }
  2853. Object.keys(storage).forEach((key) => {
  2854. if (key.indexOf(PREFIX) === 0) {
  2855. this._memory[key] = storage[key];
  2856. }
  2857. });
  2858. this.gc = bounce.time(this.gc.bind(this), 100);
  2859. }
  2860.  
  2861. gc(now = -1) {
  2862. const storage = this._storage;
  2863. now = now >= 0 ? now : Date.now();
  2864. Object.keys(storage).forEach((key, index) => {
  2865. if (key.indexOf(PREFIX) === 0) {
  2866. let item;
  2867. try {
  2868. item = JSON.parse(this._storage[key]);
  2869. } catch(e) {
  2870. storage.removeItem(key);
  2871. }
  2872. //console.info(
  2873. // `${index}, key: ${key}, expiredAt: ${new Date(item.expiredAt).toLocaleString()}, now: ${new Date(now).toLocaleString()}`);
  2874. if (item.expiredAt === '' || item.expiredAt > now) {
  2875. //console.info('not expired: ', key);
  2876. return;
  2877. }
  2878. //console.info('cache expired: ', key, item.expiredAt);
  2879. storage.removeItem(key);
  2880. }
  2881. });
  2882. }
  2883.  
  2884. setItem(key, data, expireTime) {
  2885. key = PREFIX + key;
  2886. const expiredAt =
  2887. typeof expireTime === 'number' ? (Date.now() + expireTime) : '';
  2888.  
  2889. const cacheData = {
  2890. data: data,
  2891. type: typeof data,
  2892. expiredAt: expiredAt
  2893. };
  2894.  
  2895. this._memory[key] = cacheData;
  2896. try {
  2897. this._storage[key] = JSON.stringify(cacheData);
  2898. this.gc();
  2899. } catch (e) {
  2900. if (e.name === 'QuotaExceededError' ||
  2901. e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
  2902. this.gc(0);
  2903. }
  2904. }
  2905. }
  2906.  
  2907. getItem(key) {
  2908. key = PREFIX + key;
  2909. if (!(this._storage.hasOwnProperty(key) || this._storage[key] !== undefined)) {
  2910. return null;
  2911. }
  2912. let item = null;
  2913. try {
  2914. item = JSON.parse(this._storage[key]);
  2915. } catch(e) {
  2916. delete this._memory[key];
  2917. this._storage.removeItem(key);
  2918. return null;
  2919. }
  2920.  
  2921. if (item.expiredAt === '' || item.expiredAt > Date.now()) {
  2922. return item.data;
  2923. }
  2924. return null;
  2925. }
  2926.  
  2927. removeItem(key) {
  2928. if (this._memory.hasOwnProperty(key)) {
  2929. delete this._memory[key];
  2930. }
  2931. key = PREFIX + key;
  2932. if (this._storage.hasOwnProperty(key) || this._storage[key] !== undefined) {
  2933. this._storage.removeItem(key);
  2934. }
  2935. }
  2936.  
  2937. clear() {
  2938. const storage = this._storage;
  2939. this._memory = {};
  2940. Object.keys(storage).forEach((v) => {
  2941. if (v.indexOf(PREFIX) === 0) {
  2942. storage.removeItem(v);
  2943. }
  2944. });
  2945. }
  2946. }
  2947. return CacheStorage;
  2948. })();
  2949. MylistPocket.debug.sessionCache = new CacheStorage(sessionStorage, true);
  2950. MylistPocket.debug.localCache = new CacheStorage(localStorage, true);
  2951.  
  2952. const WindowMessageEmitter = (function() {
  2953. const emitter = new Emitter();
  2954. const knownSource = [];
  2955.  
  2956. const onMessage = (event) => {
  2957. if (_.indexOf(knownSource, event.source) < 0 //&&
  2958. //event.origin !== location.protocol + '//ext.nicovideo.jp'
  2959. ) { return; }
  2960.  
  2961. try {
  2962. let data = JSON.parse(event.data);
  2963. if (data.id !== PRODUCT) { return; }
  2964.  
  2965. emitter.emit('onMessage', data.body, data.type);
  2966. } catch (e) {
  2967. console.log(
  2968. '%cMylistPocket.Error: window.onMessage - ',
  2969. 'color: red; background: yellow',
  2970. e,
  2971. event
  2972. );
  2973. console.log('%corigin: ', 'background: yellow;', event.origin);
  2974. console.log('%cdata: ', 'background: yellow;', event.data);
  2975. console.trace();
  2976. }
  2977. };
  2978.  
  2979. emitter.addKnownSource = (win) => {
  2980. knownSource.push(win);
  2981. };
  2982.  
  2983. window.addEventListener('message', onMessage);
  2984.  
  2985. return emitter;
  2986. })();
  2987.  
  2988. class CrossDomainGate extends Emitter {
  2989. static get hostReg() {
  2990. return /^[a-z0-9]*\.nicovideo\.jp$/;
  2991. }
  2992. constructor(...args) {
  2993. super();
  2994. this.initialize(...args);
  2995. }
  2996. initialize(params) {
  2997. this._baseUrl = params.baseUrl;
  2998. this._origin = params.origin || location.href;
  2999. this._type = params.type;
  3000. this._suffix = params.suffix || '';
  3001. this.name = params.name || params.type;
  3002. this._sessions = {};
  3003. this._initializeStatus = 'none';
  3004. }
  3005. _initializeFrame() {
  3006. if (this._initializeStatus !== 'none') {
  3007. return this.promise('initialize');
  3008. }
  3009. this._initializeStatus = 'initializing';
  3010. setTimeout(() => {
  3011. if (this._initializeStatus === 'done') {
  3012. return;
  3013. }
  3014. this.emitReject('initialize', {
  3015. status: 'timeout', message: `CrossDomainGate初期化タイムアウト (${this._type})`
  3016. });
  3017. }, 60 * 1000);
  3018. this._initializeCrossDomainGate();
  3019. return this.promise('initialize');
  3020. }
  3021. _initializeCrossDomainGate() {
  3022. const loaderFrame = document.createElement('iframe');
  3023. loaderFrame.referrerPolicy = 'origin';
  3024. loaderFrame.sandbox = 'allow-scripts allow-same-origin';
  3025. loaderFrame.loading = 'eager';
  3026. loaderFrame.name = `${this._type}${PRODUCT}Loader${this._suffix ? `#${this._suffix}` : ''}`;
  3027. loaderFrame.className = `xDomainLoaderFrame ${this._type}`;
  3028. loaderFrame.style.cssText = `
  3029. position: fixed; left: -100vw; pointer-events: none;user-select: none;`;
  3030. (document.body || document.documentElement).append(loaderFrame);
  3031. this._loaderWindow = loaderFrame.contentWindow;
  3032. const onInitialMessage = event => {
  3033. if (event.source !== this._loaderWindow) {
  3034. return;
  3035. }
  3036. window.removeEventListener('message', onInitialMessage);
  3037. this._onMessage(event);
  3038. };
  3039. window.addEventListener('message', onInitialMessage);
  3040. this._loaderWindow.location.replace(this._baseUrl + '#' + TOKEN);
  3041. }
  3042. _onMessage(event) {
  3043. const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
  3044. const {id, type, token, sessionId, body} = data;
  3045. if (id !== PRODUCT || type !== this._type || token !== TOKEN) {
  3046. window.console.warn('invalid token:',
  3047. {id, PRODUCT, type, _type: this._type, token, TOKEN});
  3048. return;
  3049. }
  3050. if (!this.port && body.command === 'initialized') {
  3051. const port = this.port = event.ports[0];
  3052. port.addEventListener('message', this._onMessage.bind(this));
  3053. port.start();
  3054. port.postMessage({body: {command: 'ok'}, token: TOKEN});
  3055. }
  3056. return this._onCommand(body, sessionId);
  3057. }
  3058. _onCommand({command, status, params}, sessionId = null) {
  3059. switch (command) {
  3060. case 'initialized':
  3061. if (this._initializeStatus !== 'done') {
  3062. this._initializeStatus = 'done';
  3063. const originalBody = params;
  3064. const result = this._onCommand(originalBody, sessionId);
  3065. this.emitResolve('initialize', {status: 'ok'});
  3066. return result;
  3067. }
  3068. break;
  3069. case 'message':
  3070. BroadcastEmitter.emitAsync('message', params, 'broadcast', sessionId);
  3071. break;
  3072. default: {
  3073. const session = this._sessions[sessionId];
  3074. if (!session) {
  3075. return;
  3076. }
  3077. if (status === 'ok') {
  3078. session.resolve(params);
  3079. } else {
  3080. session.reject({message: status || 'fail'});
  3081. }
  3082. delete this._sessions[sessionId];
  3083. }
  3084. break;
  3085. }
  3086. }
  3087. load(url, options) {
  3088. return this._postMessage({command: 'loadUrl', params: {url, options}});
  3089. }
  3090. videoCapture(src, sec) {
  3091. return this._postMessage({command: 'videoCapture', params: {src, sec}})
  3092. .then(result => Promise.resolve(result.params.dataUrl));
  3093. }
  3094. _fetch(url, options) {
  3095. return this._postMessage({command: 'fetch', params: {url, options}});
  3096. }
  3097. async fetch(url, options = {}) {
  3098. const result = await this._fetch(url, options);
  3099. if (typeof result === 'string' || !result.buffer || !result.init || !result.headers) {
  3100. return result;
  3101. }
  3102. const {buffer, init, headers} = result;
  3103. const _headers = new Headers();
  3104. (headers || []).forEach(a => _headers.append(...a));
  3105. const _init = {
  3106. status: init.status,
  3107. statusText: init.statusText || '',
  3108. headers: _headers
  3109. };
  3110. if (options._format === 'arraybuffer') {
  3111. return {buffer, init: _init};
  3112. }
  3113. return new Response(buffer, _init);
  3114. }
  3115. async configBridge(config) {
  3116. const keys = config.getKeys();
  3117. this._config = config;
  3118. const configData = await this._postMessage({
  3119. command: 'dumpConfig',
  3120. params: { keys, url: '', prefix: PRODUCT }
  3121. });
  3122. for (const key of Object.keys(configData)) {
  3123. config.props[key] = configData[key];
  3124. }
  3125. if (!this.constructor.hostReg.test(location.host) &&
  3126. !config.props.allowOtherDomain) {
  3127. return;
  3128. }
  3129. config.on('update', (key, value) => {
  3130. if (key === 'autoCloseFullScreen') {
  3131. return;
  3132. }
  3133. this._postMessage({command: 'saveConfig', params: {key, value, prefix: PRODUCT}}, false);
  3134. });
  3135. }
  3136. async _postMessage(body, usePromise = true) {
  3137. await this._initializeFrame();
  3138. const sessionId = 's:' + Math.random();
  3139. const {params} = body;
  3140. return this._sessions[sessionId] =
  3141. new PromiseHandler((resolve, reject) => {
  3142. try {
  3143. this.port.postMessage({body, sessionId, token: TOKEN}, params.transfer);
  3144. if (!usePromise) {
  3145. delete this._sessions[sessionId];
  3146. resolve();
  3147. }
  3148. } catch (error) {
  3149. console.log('%cException!', 'background: red;', {error, body});
  3150. delete this._sessions[sessionId];
  3151. reject(error);
  3152. }
  3153. });
  3154. }
  3155. postMessage(body, promise = true) {
  3156. return this._postMessage(body, promise);
  3157. }
  3158. sendMessage(body, usePromise = false) {
  3159. return this._postMessage({command: 'message', params: body}, usePromise);
  3160. }
  3161. pushHistory(path, title) {
  3162. return this._postMessage({command: 'pushHistory', params: {path, title}}, false);
  3163. }
  3164. async bridgeDb({name, ver, stores}) {
  3165. await this._postMessage({command: 'bridge-db', params: {name, ver, stores}});
  3166. return (command, data, storeName, transfer) => {
  3167. const params = {data, name, storeName, transfer};
  3168. return this._postMessage({command: 'bridge-db', params: {command, params}});
  3169. };
  3170. }
  3171. }
  3172.  
  3173. const CsrfTokenLoader = (() => {
  3174. const cacheStorage = new CacheStorage(
  3175. location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage);
  3176. const TIMEOUT = 10 * 1000;
  3177. const CACHE_EXPIRE_TIME = 60 * 30 * 1000;
  3178.  
  3179. class CsrfTokenLoader {
  3180. static load() {
  3181. return new Promise((resolve, reject) => {
  3182. const cache = cacheStorage.getItem('csrfToken');
  3183. if (cacheStorage.getItem('csrfToken')) {
  3184. return resolve(cache);
  3185. }
  3186.  
  3187. let timeoutTimer = window.setTimeout(() => {
  3188. reject('timeout');
  3189. }, TIMEOUT);
  3190.  
  3191. return CsrfTokenLoader._getToken().then((token) => {
  3192. window.clearTimeout(timeoutTimer);
  3193. CsrfTokenLoader.saveToCache(token);
  3194. resolve(token);
  3195. });
  3196. });
  3197. }
  3198.  
  3199. static saveToCache(token) {
  3200. cacheStorage.setItem('csrfToken', token, CACHE_EXPIRE_TIME);
  3201. }
  3202.  
  3203. static _getToken() {
  3204. const url = 'https://www.nicovideo.jp/mylist_add/video/sm9';
  3205. const tokenReg = /NicoAPI\.token *= *["']([a-z0-9-]+)["'];/;
  3206. let m;
  3207. return fetch(url, { credentials: 'include', _format: 'text'})
  3208. .then(res => res.text())
  3209. .then(result => {
  3210. if ((m = tokenReg.exec(result))) {
  3211. const token = m[1];
  3212. return Promise.resolve(token);
  3213. } else {
  3214. return Promise.reject('token parse error');
  3215. }
  3216. });
  3217. }
  3218. }
  3219.  
  3220. util.emitter.on('csrfToken', (token) => {
  3221. CsrfTokenLoader.saveToCache(token);
  3222. });
  3223.  
  3224. return CsrfTokenLoader;
  3225. })();
  3226.  
  3227. MylistPocket.debug.CsrfTokenLoader = CsrfTokenLoader;
  3228.  
  3229. const ThumbInfoLoader = (() => {
  3230. const BASE_URL = 'https://ext.nicovideo.jp/';
  3231. const MESSAGE_ORIGIN = 'https://ext.nicovideo.jp/';
  3232. const CACHE_EXPIRE_TIME = 60 * 60 * 1000;
  3233. //const CACHE_EXPIRE_TIME = 60 * 1000;
  3234. let gate = null;
  3235. let cacheStorage = new CacheStorage(sessionStorage, true);
  3236. let failedResult = {};
  3237.  
  3238. class ThumbInfoLoader {
  3239.  
  3240. constructor() {
  3241. this._emitter = new Emitter();
  3242.  
  3243. gate = new CrossDomainGate({
  3244. baseUrl: BASE_URL,
  3245. origin: MESSAGE_ORIGIN,
  3246. type: 'thumbInfo',
  3247. messager: WindowMessageEmitter
  3248. });
  3249. }
  3250.  
  3251. _onMessage(data, type) {
  3252. if (type !== 'videoInfoLoader') { return; }
  3253. const info = data.message;
  3254.  
  3255. this.emit('load', info, 'THUMB_WATCH');
  3256. }
  3257.  
  3258. _parseXml(xmlText) {
  3259. return parseThumbInfo(xmlText);
  3260. }
  3261.  
  3262. async load(watchId, options = {}) {
  3263. const cacheKey = `thumbInfo_${watchId}`;
  3264. const cache = cacheStorage.getItem(cacheKey);
  3265.  
  3266. if (failedResult[`${watchId}`]) {
  3267. return Promise.reject({data: failedResult[`${watchId}`], watchId});
  3268. }
  3269. if (cache) {
  3270. return cache;
  3271. }
  3272.  
  3273. const thumbInfo =
  3274. await gate.fetch(`${BASE_URL}api/getthumbinfo/${watchId}`, options)
  3275. .catch(e => { return {status: 'fail', message: e.message || `gate.fetch('${watchId}') failed` }; });
  3276. thumbInfo.fromCache = !!cache;
  3277. if (thumbInfo.status !== 'ok') {
  3278. failedResult[`${watchId}`] = thumbInfo;
  3279. return Promise.reject(thumbInfo);
  3280. }
  3281. cacheStorage.setItem(cacheKey, thumbInfo, CACHE_EXPIRE_TIME);
  3282. return thumbInfo;
  3283. }
  3284. }
  3285.  
  3286. const loader = new ThumbInfoLoader();
  3287. return {
  3288. load: watchId => loader.load(watchId),
  3289. loadOwnerInfo: async watchId => {
  3290. const info = await loader.load(watchId);
  3291. const owner = info.owner;
  3292. if (!owner) {
  3293. return {};
  3294. }
  3295.  
  3296. const lang = util.getPageLanguage();
  3297. const prefix = owner.type === 'user' ? '投稿者: ' : '提供: ';
  3298. const suffix =
  3299. (owner.type === 'user' && lang === 'ja-JP') ? ' さん' : '';
  3300. owner.linkId =
  3301. owner.id ?
  3302. (owner.type === 'user' ? `user/${owner.id}` : `ch${owner.id}`) :
  3303. '';
  3304. owner.localeName = `${prefix}${owner.name}${suffix}`;
  3305. return owner;
  3306. }
  3307. };
  3308.  
  3309. })();
  3310.  
  3311. MylistPocket.debug.ThumbInfoLoader = ThumbInfoLoader;
  3312.  
  3313.  
  3314.  
  3315. const DeflistApiLoader = ((CsrfTokenLoader) => {
  3316. const cacheStorage = new CacheStorage(
  3317. location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage);
  3318. const TIMEOUT = 30000;
  3319. const CACHE_EXPIRE_TIME = 60 * 3 * 1000;
  3320. let isZenzaReady = false;
  3321.  
  3322. class DeflistApiLoader {
  3323.  
  3324. static getItems() {
  3325. const url = 'https://www.nicovideo.jp/api/deflist/list';
  3326. const cacheKey = 'deflistItems';
  3327.  
  3328. return new Promise(function(resolve, reject) {
  3329.  
  3330. const cache = cacheStorage.getItem(cacheKey);
  3331. if (cache) {
  3332. window.setTimeout(() => {
  3333. resolve({items: cache.mylistitem, status: cache.status, from: 'cache'});
  3334. }, 0);
  3335. return;
  3336. }
  3337.  
  3338. let timeoutTimer = window.setTimeout(() => {
  3339. timeoutTimer = null;
  3340. reject({status: 'fail', description: 'timeout'});
  3341. }, TIMEOUT);
  3342.  
  3343. fetch(url, {
  3344. credentials: 'include'
  3345. }).then((res) => {
  3346. return res.json();
  3347. }).then((json) => {
  3348. if (json.status !== 'ok') {
  3349. return reject(json);
  3350. }
  3351.  
  3352. if (timeoutTimer) { window.clearTimeout(timeoutTimer);
  3353. } else { return; }
  3354.  
  3355. cacheStorage.setItem(cacheKey, json, CACHE_EXPIRE_TIME);
  3356. resolve({items: json.mylistitem, status: json.status, from: 'fetch'});
  3357. });
  3358. });
  3359. }
  3360.  
  3361. static findItemByWatchId(watchId) {
  3362. return DeflistApiLoader.getItems().then(({items}) => {
  3363. for (let i = 0, len = items.length; i < len; i++) {
  3364. let item = items[i], wid = item.id || item.item_data.watch_id;
  3365. if (wid === watchId) {
  3366. return Promise.resolve(item);
  3367. }
  3368. }
  3369. return Promise.reject();
  3370. });
  3371. }
  3372.  
  3373. static _removeItem({watchId, token}) {
  3374. const cacheKey = 'deflistItems';
  3375. DeflistApiLoader.findItemByWatchId(watchId).then((item) => {
  3376. const url = 'https://www.nicovideo.jp/api/deflist/delete';
  3377. const body = 'id_list[0][]=' + item.item_id + '&token=' + token;
  3378.  
  3379. const req = {
  3380. credentials: 'include',
  3381. method: 'post',
  3382. body,
  3383. headers: {'Content-Type': 'application/x-www-form-urlencoded'}
  3384. };
  3385.  
  3386. return fetch(url, req)
  3387. .then(res => { return res.json(); })
  3388. .then((result) => {
  3389. if (result.status !== 'ok') {
  3390. return Promise.reject({
  3391. status: 'fail',
  3392. result: result,
  3393. code: result.error.code,
  3394. message: result.error.description
  3395. });
  3396. }
  3397.  
  3398.  
  3399. cacheStorage.removeItem(cacheKey);
  3400. util.emitter.emitAsync('deflistRemove', watchId);
  3401. return Promise.resolve({
  3402. status: 'ok',
  3403. result: result,
  3404. message: 'とりあえずマイリストから削除'
  3405. });
  3406.  
  3407. }, (err) => {
  3408. return Promise.reject({
  3409. result: err,
  3410. message: 'とりあえずマイリストから削除失敗(2)'
  3411. });
  3412. });
  3413.  
  3414. }, (err) => {
  3415. return Promise.reject({
  3416. status: 'fail',
  3417. result: err,
  3418. message: '動画が見つかりません'
  3419. });
  3420. });
  3421. }
  3422.  
  3423. static removeItem(watchId) {
  3424. return CsrfTokenLoader.load().then((token) => {
  3425. return DeflistApiLoader._removeItem({watchId, token});
  3426. });
  3427. }
  3428.  
  3429. static __addItem({watchId, description, token, isRetry = false}) {
  3430. const cacheKey = 'deflistItems';
  3431. const url = 'https://www.nicovideo.jp/api/deflist/add';
  3432. let body = 'item_id=' + watchId + '&token=' + token;
  3433. if (description) {
  3434. body += '&description='+ encodeURIComponent(description);
  3435. }
  3436.  
  3437. const req = {
  3438. method: 'post',
  3439. credentials: 'include',
  3440. body,
  3441. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  3442. };
  3443.  
  3444. return new Promise((resolve, reject) => {
  3445. fetch(url, req)
  3446. .then((res) => { return res.json(); })
  3447. .then((result) => {
  3448.  
  3449. if (result.status && result.status === 'ok') {
  3450. cacheStorage.removeItem(cacheKey);
  3451. //ZenzaWatch.emitter.emitAsync('deflistAdd', watchId, description);
  3452. return resolve({
  3453. status: 'ok',
  3454. result: result,
  3455. message: 'とりあえずマイリスト登録'
  3456. });
  3457. }
  3458.  
  3459. if (!result.status || !result.error) {
  3460. return reject({
  3461. status: 'fail',
  3462. result: result,
  3463. message: 'とりあえずマイリスト登録失敗(100)'
  3464. });
  3465. }
  3466.  
  3467. if (result.error.code !== 'EXIST' || isRetry) {
  3468. return reject({
  3469. status: 'fail',
  3470. result: result,
  3471. code: result.error.code,
  3472. message: result.error.description
  3473. });
  3474. }
  3475.  
  3476. /**
  3477. * すでに登録されている場合は、いったん削除して再度追加(先頭に移動)
  3478. */
  3479. return DeflistApiLoader.removeItem(watchId)
  3480. .then(util.getSleepPromise(1500, 'deflist remove'))
  3481. .then(() => {
  3482. return DeflistApiLoader._addItem(watchId, description, true)
  3483. .then((result) => {
  3484. resolve({
  3485. status: 'ok',
  3486. result: result,
  3487. message: 'とりあえずマイリストの先頭に移動'
  3488. });
  3489. });
  3490. }, (err) => {
  3491.  
  3492. reject({
  3493. status: 'fail',
  3494. result: err.result,
  3495. code: err.code,
  3496. message: 'とりあえずマイリスト登録失敗(101)'
  3497. });
  3498. });
  3499.  
  3500. }, (err) => {
  3501. reject({
  3502. status: 'fail',
  3503. result: err,
  3504. message: 'とりあえずマイリスト登録失敗(200)'
  3505. });
  3506. });
  3507. });
  3508. }
  3509.  
  3510. static _addItem(watchId, description, isRetry = false) {
  3511. return CsrfTokenLoader.load().then((token) => {
  3512. return DeflistApiLoader.__addItem({watchId, description, isRetry, token});
  3513. });
  3514. }
  3515.  
  3516. static addItem(watchId, description) {
  3517. return DeflistApiLoader._addItem(watchId, description, false);
  3518. }
  3519.  
  3520. static addItemWithOwnerName(watchId) {
  3521. return ThumbInfoLoader.loadOwnerInfo(watchId).then((owner) => {
  3522. if (!owner.id) {
  3523. return DeflistApiLoader.addItem(watchId);
  3524. }
  3525.  
  3526. const description = `${owner.localeName} ${owner.linkId}`;
  3527. return DeflistApiLoader.addItem(watchId, description);
  3528. }, () => DeflistApiLoader.addItem(watchId));
  3529. }
  3530.  
  3531. static clearCache() {
  3532. cacheStorage.removeItem('deflistItems');
  3533. }
  3534.  
  3535. }
  3536.  
  3537. ZenzaDetector.detect().then((ZenzaWatch) => {
  3538. isZenzaReady = true;
  3539. ZenzaWatch.emitter.on('deflistRemove', () => DeflistApiLoader.clearCache());
  3540. });
  3541.  
  3542. //DeflistApiLoader.clearCache();
  3543.  
  3544. return DeflistApiLoader;
  3545. })(CsrfTokenLoader);
  3546.  
  3547. MylistPocket.debug.DeflistApiLoader = DeflistApiLoader;
  3548.  
  3549. class HoverMenu extends Emitter {
  3550. constructor() {
  3551. super();
  3552. this._init();
  3553. }
  3554.  
  3555. _init() {
  3556. this._view = document.querySelector('.mylistPocketHoverMenu');
  3557.  
  3558. this._view.addEventListener('click', this._onClick.bind(this));
  3559. this._view.addEventListener('mousedown', this._onMousedown.bind(this));
  3560. this._view.addEventListener('contextmenu', this._onContextMenu.bind(this));
  3561.  
  3562. this._onHoverEnd = bounce.time(this._onHoverEnd.bind(this), 500);
  3563. document.body.addEventListener(
  3564. 'mouseover', this._onHover.bind(this), {passive: true});
  3565. document.body.addEventListener(
  3566. 'mouseout', this._onMouseout.bind(this), {passive: true});
  3567. document.body.addEventListener(
  3568. 'mouseover', this._onHoverEnd, {passive: true});
  3569. document.body.addEventListener(
  3570. 'click', () => { this.hide(); }, {passive: true});
  3571.  
  3572.  
  3573. util.emitter.on('hideHover', () => this.hide());
  3574.  
  3575. this._x = this._y = 0;
  3576.  
  3577. ZenzaDetector.detect().then(ZenzaWatch => {
  3578. this._isZenzaReady = true;
  3579. this.addClass('is-zenzaReady');
  3580. ZenzaWatch.emitter.on('DialogPlayerOpen', bounce.time(() => {
  3581. this.hide();
  3582. }, 1000));
  3583. });
  3584.  
  3585. this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp');
  3586. this.toggleClass('is-guest', !util.isLogin());
  3587. this._deflistButton = this._view.querySelector('.mylistPocketButton.deflist-add');
  3588. MylistPocket.debug.hoverMenu = this._view;
  3589. }
  3590.  
  3591. toggleClass(className, v) {
  3592. className.split(/ +/).forEach((c) => {
  3593. this._view.classList.toggle(c, v);
  3594. });
  3595. }
  3596.  
  3597. addClass(className) { this.toggleClass(className, true); }
  3598. removeClass(className) { this.toggleClass(className, false); }
  3599.  
  3600. hide() {
  3601. this.removeClass('is-show');
  3602. }
  3603.  
  3604. show() {
  3605. this.addClass('is-show');
  3606. }
  3607.  
  3608. moveTo(x, y) {
  3609. this._x = x;
  3610. this._y = y;
  3611. this._view.style.left = x + 'px';
  3612. this._view.style.top = y + 'px';
  3613. }
  3614.  
  3615. _onClick(e) {
  3616. e.preventDefault();
  3617. e.stopPropagation();
  3618. }
  3619.  
  3620. _onContextMenu(e) {
  3621. e.preventDefault();
  3622. e.stopPropagation();
  3623. }
  3624.  
  3625. _onMousedown(e) {
  3626. const watchId = this._watchId;
  3627. const target = e.target.classList.contains('command') ?
  3628. e.target : e.target.closest('.command');
  3629. const command = target.getAttribute('data-command');
  3630. e.preventDefault();
  3631. e.stopPropagation();
  3632.  
  3633. if (command === 'info') {
  3634. this._videoInfo(watchId);
  3635. this.hide();
  3636. } else if (command === 'playlist-queue') {
  3637. this.emit('playlist-queue', watchId, this);
  3638. } else {
  3639. if (e.button !== 0 || e.shiftKey) {
  3640. this._deflistRemove(watchId);
  3641. } else {
  3642. this._deflist(watchId);
  3643. }
  3644. }
  3645. }
  3646.  
  3647. _videoInfo(watchId) {
  3648. this.emit('info', watchId || this._watchId, this);
  3649. }
  3650.  
  3651. _deflist(watchId) {
  3652. this.emit('deflist-add', watchId || this._watchId, this);
  3653. }
  3654.  
  3655. _deflistRemove(watchId) {
  3656. this.emit('deflist-remove', watchId || this._watchId, this);
  3657. }
  3658.  
  3659. _onHover(e) {
  3660. const target = this._isTargetElement(e);
  3661. if (!target) { return; }
  3662.  
  3663. this._hoverElement = target;
  3664. }
  3665.  
  3666. _onHoverEnd(e) {
  3667. const target =
  3668. e.target.tagName === 'A' ? e.target : e.target.closest('a');
  3669. if (!target || this._hoverElement !== target) { return; }
  3670. const href = target.getAttribute('data-href') || target.getAttribute('href');
  3671. const watchId = target.dataset.nicoVideoId || util.getWatchId(href);
  3672. const offset = target.getBoundingClientRect();
  3673. //const bodyOffset = document.body.getBoundingClientRect();
  3674. const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
  3675. const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0;
  3676. const left = offset.left + scrollLeft;
  3677. const top = offset.top + scrollTop;
  3678. const host = target.hostname;
  3679. if (host !== 'www.nicovideo.jp' && host !== 'nico.ms' && host !== 'sp.nicovideo.jp') { return; }
  3680.  
  3681. if (target.classList.contains('noHoverMenu')) { return; }
  3682. if (!watchId || !watchId.match(/^[a-z0-9]+$/)) { return; }
  3683. if (watchId.indexOf('lv') === 0) { return; }
  3684.  
  3685. this._watchId = watchId;
  3686. this.show();
  3687. this.moveTo(
  3688. left + target.offsetWidth - this._view.offsetWidth / 2,
  3689. top + target.offsetHeight / 2 - this._view.offsetHeight / 2
  3690. );
  3691. }
  3692.  
  3693. _onMouseout(e) {
  3694. const target = this._isTargetElement(e);
  3695. if (!target) { return; }
  3696.  
  3697. if (this._hoverElement === e.target) {
  3698. this._hoverElement = null;
  3699. }
  3700. }
  3701.  
  3702. _isTargetElement(e) {
  3703. const target =
  3704. e.target.tagName === 'A' ? e.target : e.target.closest('a');
  3705. if (!target) { return false; }
  3706. const href = target.href || '';
  3707. if (!/(watch\/[a-z0-9]+|nico\.ms\/[a-z0-9]+)/.test(href)) { return false; }
  3708. return target;
  3709. }
  3710.  
  3711. set isBusy(v) {
  3712. this._isBusy = v;
  3713. this.toggleClass('is-busy', v);
  3714. }
  3715.  
  3716. get isBusy() {
  3717. return !!this._isBusy;
  3718. }
  3719.  
  3720. notifyBeginDeflistUpdate(/*watchId*/) {
  3721. this.addClass('is-deflistUpdating');
  3722. }
  3723.  
  3724. notifyEndDeflistUpdate(result) {
  3725. this.addClass('is-deflistSuccess');
  3726. window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000);
  3727.  
  3728. this._deflistButton.setAttribute('data-result', result.message || '登録しました');
  3729. this.removeClass('is-deflistUpdating');
  3730. }
  3731.  
  3732. notifyFailDeflistUpdate(result) {
  3733. this.addClass('is-deflistFail');
  3734. window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
  3735.  
  3736. this._deflistButton.setAttribute('data-result', result.message || '登録失敗');
  3737. this.removeClass('is-deflistUpdating');
  3738. }
  3739. }
  3740.  
  3741.  
  3742. class VideoInfoView extends Emitter {
  3743. constructor({host, tpl}) {
  3744. super();
  3745. this._host = host;
  3746. this._tpl = tpl;
  3747. this._slot = {};
  3748.  
  3749. this._baseConfig = config;
  3750. this._config = config.namespace('videoInfo');
  3751. this._mylistConfig = config.namespace('mylist');
  3752. const ngConfig = this._ngConfig = config.namespace('ng');
  3753. const favConfig = this._favConfig = config.namespace('fav');
  3754. this._nicoadConfig = config.namespace('nicoad');
  3755.  
  3756. const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
  3757. this._ngChecker = ngChecker;
  3758. this._favChecker = favChecker;
  3759. }
  3760.  
  3761. _initialize() {
  3762. if (this._isInitialized) { return; }
  3763. const host = this._host;
  3764. const tpl = this._tpl;
  3765.  
  3766. this._shadowRoot = util.attachShadowDom({host, tpl});
  3767. Array.prototype.forEach.call(this._host.querySelectorAll('*'), (elm) => {
  3768. //this._host.querySelectorAll('*').forEach((elm) => {
  3769. const slot = elm.getAttribute('slot');
  3770. if (!slot) { return; }
  3771. //const type = elm.getAttribute('data-type') || 'string';
  3772. this._slot[slot] = elm;
  3773. });
  3774.  
  3775. this._rootDom = this._shadowRoot.querySelector('.root');
  3776. this._hostDom = this._host;
  3777.  
  3778. this._rootDom.addEventListener('mousedown', e => { e.stopPropagation(); });
  3779. this._shadowRoot.addEventListener('mousedown', e => { e.stopPropagation(); });
  3780. this._rootDom.querySelector('.setting-panel-main').addEventListener('click', e => {
  3781. e.stopPropagation();
  3782. });
  3783.  
  3784. this._initSettingPanel();
  3785.  
  3786. const updateNgEnable = v => { this.toggleClass('is-ng-enable', v); };
  3787. updateNgEnable(this._ngConfig.props.enable);
  3788. this._ngConfig.onkey('enable', updateNgEnable);
  3789.  
  3790. this._rootDom.addEventListener('click', this._onClick.bind(this));
  3791.  
  3792. this._boundOnBodyMouseDown = this._onBodyMouseDown.bind(this);
  3793.  
  3794. MylistPocket.debug.view = this;
  3795.  
  3796. util.emitter.on('hideHover', () => {
  3797. this.hide();
  3798. });
  3799.  
  3800. const debUpdateFavNg = bounce.time(this._updateFavNg.bind(this), 100);
  3801. this._ngConfig .on('update', debUpdateFavNg);
  3802. this._favConfig .on('update', debUpdateFavNg);
  3803. //this._mylistConfig.on('update', debUpdateFavNg);
  3804.  
  3805. ZenzaDetector.detect().then(() => {
  3806. this._isZenzaReady = true;
  3807. this.addClass('is-zenzaReady');
  3808. window.ZenzaWatch.emitter.on('DialogPlayerOpen', bounce.time(() => {
  3809. this.hide();
  3810. }, 1000));
  3811. });
  3812.  
  3813. this._videoInfoArea = this._rootDom.querySelector('.video-info');
  3814. this._deflistButton =
  3815. this._rootDom.querySelector('.mylistPocketButton.deflist-add');
  3816.  
  3817. this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp');
  3818. this.toggleClass('is-firefox', util.isFirefox());
  3819.  
  3820. MylistPocket.external.observe({
  3821. query: 'a.videoLink',
  3822. container: this._hostDom.querySelector('.description'),
  3823. });
  3824.  
  3825. this._isInitialized = true;
  3826. }
  3827.  
  3828. _initSettingPanel() {
  3829. const onSettingFormChange = this._onSettingFormChange.bind(this);
  3830.  
  3831. const refresh = () => {
  3832. Array.from(this._rootDom.querySelectorAll('.setting-form')).forEach(elm => {
  3833. const name = elm.getAttribute('data-config-name');
  3834. if (!name) { return; }
  3835. const namespace = elm.getAttribute('data-config-namespace') || '';
  3836. let config = this._config;
  3837. switch (namespace) {
  3838. case 'ng':
  3839. config = this._ngConfig;
  3840. break;
  3841. case 'fav':
  3842. config = this._favConfig;
  3843. break;
  3844. case 'mylist':
  3845. config = this._mylistConfig;
  3846. break;
  3847. case 'nicoad':
  3848. config = this._nicoadConfig;
  3849. break;
  3850. default:
  3851. config = this._baseConfig;
  3852. }
  3853. const tagName = (elm.tagName.toLowerCase()).toLowerCase();
  3854. if (tagName === 'input') {
  3855. const type = (elm.type || '').toLowerCase();
  3856. switch (type) {
  3857. case 'checkbox':
  3858. elm.checked = !!config.props[name];
  3859. break;
  3860. default:
  3861. elm.value = config.props[name];
  3862. break;
  3863. }
  3864. } else if (tagName === 'select' || tagName === 'textarea') {
  3865. elm.value = config.props[name];
  3866. }
  3867.  
  3868. elm.removeEventListener('change', onSettingFormChange);
  3869. elm.addEventListener('change', onSettingFormChange);
  3870. });
  3871. };
  3872.  
  3873. const onUpdate = bounce.time(refresh, 100);
  3874.  
  3875. const syncZenza = bounce.time(() => {
  3876. if (!this._ngConfig.props.syncZenza || !this._isZenzaReady) { return; }
  3877. window.ZenzaWatch.config.setValue('videoTagFilter', this._ngConfig.props.tag);
  3878. window.ZenzaWatch.config.setValue('videoOwnerFilter', this._ngConfig.props.owner);
  3879. }, 1000);
  3880.  
  3881. refresh();
  3882.  
  3883. this._config.on('update', onUpdate);
  3884. this._favConfig.on('update', onUpdate);
  3885. this._ngConfig.on('update', () => {
  3886. onUpdate();
  3887. syncZenza();
  3888. });
  3889.  
  3890. }
  3891.  
  3892. _onSettingFormChange(e) {
  3893. const elm = e.target;
  3894. const name = elm.getAttribute('data-config-name');
  3895. if (!name) { return; }
  3896. const namespace = elm.getAttribute('data-config-namespace') || '';
  3897. let config = this._config;
  3898. switch (namespace) {
  3899. case 'ng':
  3900. config = this._ngConfig;
  3901. break;
  3902. case 'fav':
  3903. config = this._favConfig;
  3904. break;
  3905. case 'mylist':
  3906. config = this._mylistConfig;
  3907. break;
  3908. case 'nicoad':
  3909. config = this._nicoadConfig;
  3910. break;
  3911. default:
  3912. config = this._baseConfig;
  3913. }
  3914.  
  3915. const tagName = (elm.tagName.toLowerCase()).toLowerCase();
  3916. if (tagName === 'input') {
  3917. const type = (elm.type || '').toLowerCase();
  3918. switch (type) {
  3919. case 'checkbox':
  3920. config.props[name] = elm.checked;
  3921. break;
  3922. default:
  3923. config.props[name] = elm.value;
  3924. break;
  3925. }
  3926. } else if (tagName === 'select' || tagName === 'textarea') {
  3927. config.props[name] = elm.value;
  3928. }
  3929. }
  3930.  
  3931. toggleClass(className, v) {
  3932. className.split(/ +/).forEach((c) => {
  3933. this._rootDom.classList.toggle(c, v);
  3934. this._hostDom.classList.toggle(c, v);
  3935. });
  3936. }
  3937.  
  3938. addClass(className) { this.toggleClass(className, true); }
  3939. removeClass(className) { this.toggleClass(className, false); }
  3940.  
  3941. bind(videoInfo) {
  3942. this._videoInfo = videoInfo;
  3943. if (videoInfo.status === 'ok') {
  3944. this._bindSuccess(videoInfo);
  3945. } else {
  3946. this._bindFail(videoInfo);
  3947. }
  3948. window.setTimeout(() => {
  3949. this.removeClass('is-loading');
  3950. }, 0);
  3951. }
  3952.  
  3953. _onClick(e) {
  3954. const t = e.target;
  3955. const elm =
  3956. t.classList.contains('command') ?
  3957. t : e.target.closest('.command');
  3958. if (!elm) { return; }
  3959.  
  3960. // 簡易 throttle
  3961. if (elm.classList.contains('is-active')) { return; }
  3962. elm.classList.add('is-active');
  3963. window.setTimeout(() => { elm.classList.remove('is-active'); }, 500);
  3964.  
  3965. e.preventDefault();
  3966. e.stopPropagation();
  3967. const command = elm.getAttribute('data-command');
  3968. const param = elm.getAttribute('data-param');
  3969. switch (command) {
  3970. case 'toggle-setting':
  3971. this.toggleSettingPanel();
  3972. break;
  3973. case 'add-ng-tag': case 'add-fav-tag':
  3974. case 'toggle-ng-tag': case 'toggle-fav-tag': {
  3975. const tag = elm.getAttribute('data-tag') || '';
  3976. if (!tag) { break; }
  3977. this.emit('command', command, {
  3978. watchId: this._videoInfo.watchId,
  3979. value: tag
  3980. }, this);
  3981. }
  3982. break;
  3983. case 'add-ng-owner': case 'add-fav-owner':
  3984. case 'toggle-ng-owner': case 'toggle-fav-owner': {
  3985. let owner =
  3986. (this._videoInfo.isChannel ? 'ch' : '') +
  3987. this._videoInfo.ownerId + '#' + this._videoInfo.ownerName;
  3988. this.emit('command', command, {
  3989. watchId: this._videoInfo.watchId,
  3990. value: owner
  3991. }, this);
  3992. }
  3993. break;
  3994. case 'mylist-comment-open':
  3995. this.emit('command', command, this._videoInfo.watchId);
  3996. break;
  3997. case 'close':
  3998. this.hide();
  3999. break;
  4000. default:
  4001. this.emit('command', command, param, this);
  4002. }
  4003. }
  4004.  
  4005. _updateFavNg() {
  4006. if (!this._isInitialized) { return; }
  4007. if (!this._videoInfo || this._videoInfo.status !== 'ok') { return; }
  4008.  
  4009. const videoInfo = this._videoInfo;
  4010. const ownerInfo = this._rootDom.querySelector('.owner-info');
  4011. ownerInfo.classList.toggle('is-favorited',
  4012. this._favChecker.isMatchOwner(videoInfo.owner));
  4013. ownerInfo.classList.toggle('is-ng',
  4014. this._ngChecker .isMatchOwner(videoInfo.owner));
  4015.  
  4016. Array.prototype.forEach.call(
  4017. this._rootDom.querySelectorAll('.tag-container'),
  4018. (elm) => {
  4019. const tag = elm.getAttribute('data-tag');
  4020. elm.classList.toggle('is-favorited', this._favChecker.isMatchTag(tag));
  4021. elm.classList.toggle('is-ng', this._ngChecker.isMatchTag(tag));
  4022. });
  4023. }
  4024.  
  4025. toggleSettingPanel() {
  4026. this.toggleClass('is-setting');
  4027. }
  4028.  
  4029. _onBodyMouseDown() {
  4030. document.body.removeEventListener('mousedown', this._boundOnBodyMouseDown);
  4031. this.hide();
  4032. }
  4033.  
  4034. reset() {
  4035. this._initialize();
  4036. window.setTimeout(() => { this._videoInfoArea.scrollTop = 0; }, 0);
  4037. this.removeClass('noclip');
  4038. this.addClass('is-loading');
  4039. }
  4040.  
  4041. show() {
  4042. this.addClass('show');
  4043. document.body.addEventListener('mousedown', this._boundOnBodyMouseDown);
  4044. }
  4045.  
  4046. hide() {
  4047. this._videoInfoArea.scrollTop = 0;
  4048. this.removeClass('show is-ok is-fail noclip is-setting');
  4049. }
  4050.  
  4051. _bindSuccess(videoInfo) {
  4052. const toCamel = p => {
  4053. return p.replace(/-./g, s => { return s.charAt(1).toUpperCase(); });
  4054. };
  4055.  
  4056. Object.keys(this._slot).forEach((key) => {
  4057. const camelKey = toCamel(key);
  4058. const data = videoInfo[camelKey];
  4059.  
  4060. const elm = this._slot[key];
  4061. const type = elm.getAttribute('data-type') || 'string';
  4062. switch (type) {
  4063. case 'html':
  4064. this._createDescription(elm, data);
  4065. break;
  4066. case 'int': {
  4067. let i = parseInt(data, 10);
  4068. i = i.toLocaleString ? i.toLocaleString() : i;
  4069. elm.textContent = i;
  4070. }
  4071. break;
  4072. case 'link':
  4073. elm.href = data;
  4074. break;
  4075. case 'image':
  4076. elm.src = data.replace('http:', 'https:');
  4077. break;
  4078. case 'date':
  4079. elm.textContent = data.toLocaleString();
  4080. break;
  4081. default:
  4082. elm.textContent = data;
  4083. }
  4084. });
  4085.  
  4086. const df = document.createDocumentFragment();
  4087. //Array.prototype.forEach.call(this._host.querySelectorAll('.tag'), t => { t.remove(); });
  4088. videoInfo.tags.forEach(tag => {
  4089. df.appendChild((this._createTagSlot(tag, videoInfo)));
  4090. });
  4091. const videoTags = this._rootDom.querySelector('.video-tags');
  4092. videoTags.innerHTML = '';
  4093. videoTags.appendChild(df);
  4094.  
  4095. Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-watch-id'), elm => {
  4096. elm.setAttribute('data-param', videoInfo.watchId);
  4097. });
  4098. Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-video-id'), elm => {
  4099. elm.setAttribute('data-param', videoInfo.videoId);
  4100. });
  4101.  
  4102. const target = this._config.props.openNewWindow ? '_blank' : '_self';
  4103. Array.prototype.forEach.call(
  4104. this._host.querySelectorAll('.target-change'), elm => {
  4105. elm.target = target;
  4106. elm.rel = 'noopener';
  4107. });
  4108.  
  4109. this._updateFavNg();
  4110.  
  4111. this.toggleClass('is-channel', videoInfo.isChannel);
  4112. this.addClass('is-ok');
  4113. this.removeClass('is-fail');
  4114. window.setTimeout(() => { this.addClass('noclip'); }, 800);
  4115. }
  4116.  
  4117. _createDescription(elm, data) {
  4118. elm.innerHTML = util.httpLink(data);
  4119. const watchReg = /watch\/([a-z0-9]+)/;
  4120. const isZenzaReady = this._isZenzaReady;
  4121. //if (util.isFirefox()) { return; }
  4122. Array.from(elm.querySelectorAll('.videoLink[href*=\'watch/\']')).forEach((link) => {
  4123. const href = link.getAttribute('href');
  4124. if (!watchReg.test(href)) { return; }
  4125. const watchId = RegExp.$1;
  4126. if (isZenzaReady) {
  4127. link.classList.add('noHoverMenu');
  4128. link.classList.add('command');
  4129. link.setAttribute('data-command', 'zenza-open');
  4130. link.setAttribute('data-param', watchId);
  4131. }
  4132. const label = document.createElement('span');
  4133. label.className = 'label';
  4134. label.textContent = link.textContent;
  4135. link.textContent = '';
  4136. link.append(label);
  4137.  
  4138. const btn = document.createElement('button');
  4139. btn.innerHTML = '?';
  4140. btn.className = 'command command-button noHoverMenu';
  4141. btn.setAttribute('slot', 'command-button');
  4142. btn.setAttribute('tooltip', '動画情報');
  4143. btn.setAttribute('data-command', 'info');
  4144. btn.setAttribute('data-param', watchId);
  4145. link.appendChild(btn);
  4146.  
  4147. const thumbnail = util.getThumbnailUrlByVideoId(watchId);
  4148. const img = document.createElement('img');
  4149. img.className = 'videoThumbnail preview';
  4150. img.src = 'https://nicovideo.cdn.nimg.jp/uni/img/common/video_deleted.jpg';//(thumbnail || '').replace(/^http:/, '');
  4151. link.classList.add('popupThumbnail');
  4152. link.appendChild(img);
  4153.  
  4154. link.dataset.videoId = watchId;
  4155. link.classList.add('watch');
  4156. });
  4157. }
  4158.  
  4159. _bindFail(videoInfo) {
  4160. this._slot['error-description'].textContent =
  4161. `動画情報の取得に失敗しました (${videoInfo.description})`;
  4162. this.addClass('is-fail');
  4163. this.removeClass('is-ok');
  4164. }
  4165.  
  4166.  
  4167. _createTagSlot(tag, {isChannel, owner}) {
  4168. const text = util.escapeHtml(tag.text);
  4169. const lock = tag.isLocked ? 'is-locked' : '';
  4170. const span = document.createElement('span');
  4171. const ownerId = owner ? owner.id : '';
  4172.  
  4173. const a = document.createElement('a');
  4174. const target = this._config.props.openNewWindow ? '_blank' : '_self';
  4175. a.textContent = tag.text;
  4176. a.className = `tag ${lock}`;
  4177. a.target = target;
  4178. a.rel = 'noopener';
  4179. a.href = `https://www.nicovideo.jp/tag/${encodeURIComponent(text)}`;
  4180. span.appendChild(a);
  4181.  
  4182. if (isChannel) {
  4183. const ch = document.createElement('a');
  4184. const target = this._config.props.openNewWindow ? '_blank' : '_self';
  4185. ch.textContent = '[ch]';
  4186. ch.className = `tag ${lock} channel-search`;
  4187. ch.target = target;
  4188. ch.rel = 'noopener';
  4189. ch.title = 'チャンネル検索';
  4190. //ch.href = `http://ch.nicovideo.jp/search/${encodeURIComponent(text)}?channel_id=ch${ownerId}&type=video&mode=t`;
  4191. ch.href = `https://ch.nicovideo.jp/search/${encodeURIComponent(text)}?type=video&mode=t`;
  4192. span.appendChild(ch);
  4193. }
  4194.  
  4195. const fav = document.createElement('button');
  4196. fav.className = 'add-fav-button command';
  4197. fav.setAttribute('data-command', 'toggle-fav-tag');
  4198. fav.setAttribute('data-tag', tag.text);
  4199. fav.innerHTML = '★'; //'&#8416;'; // &#x2716;
  4200. span.appendChild(fav);
  4201.  
  4202. const bt = document.createElement('button');
  4203. bt.className = 'add-ng-button command';
  4204. bt.setAttribute('data-command', 'toggle-ng-tag');
  4205. bt.setAttribute('data-tag', tag.text);
  4206. bt.innerHTML = '&#x2716;'; //'&#8416;'; // &#x2716;
  4207. span.appendChild(bt);
  4208.  
  4209. const menu = `<zenza-tag-item-menu
  4210. class="tagItemMenu"
  4211. data-text="${encodeURIComponent(text)}"
  4212. data-has-nicodic="0"
  4213. ></zenza-tag-item-menu>`;
  4214. span.insertAdjacentHTML('afterbegin', menu);
  4215.  
  4216. span.className = 'tag-container';
  4217. span.setAttribute('data-tag', tag.text);
  4218. span.slot = 'tag';
  4219. return span;
  4220. }
  4221.  
  4222. notifyBeginDeflistUpdate(/*watchId*/) {
  4223. this.addClass('is-deflistUpdating');
  4224. }
  4225.  
  4226. notifyEndDeflistUpdate(result) {
  4227. this.addClass('is-deflistSuccess');
  4228. window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000);
  4229.  
  4230. this._deflistButton.setAttribute('data-result', result.message || '登録しました');
  4231. this.removeClass('is-deflistUpdating');
  4232. }
  4233.  
  4234. notifyFailDeflistUpdate(result) {
  4235. this.addClass('is-deflistFail');
  4236. window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
  4237.  
  4238. this._deflistButton.setAttribute('data-result', result.message || '登録失敗');
  4239. this.removeClass('is-deflistUpdating');
  4240. }
  4241. }
  4242.  
  4243.  
  4244. class VideoInfo {
  4245. static createByThumbInfo(thumbInfo) {
  4246. let thumbnail = thumbInfo.thumbnail;
  4247. if (util.hasLargeThumbnail(thumbInfo.videoId)) {
  4248. thumbnail = thumbnail.replace(/\.[ML]$/) + '.L';
  4249. }
  4250. const owner = thumbInfo.owner || {};
  4251. const isChannel = thumbInfo.isChannel;
  4252. const rawData = {
  4253. status: thumbInfo.status,
  4254. videoId: thumbInfo.id,
  4255. watchId: thumbInfo.v,
  4256. videoTitle: thumbInfo.title,
  4257. videoThumbnail: thumbnail,
  4258. uploadDate: thumbInfo.postedAt,
  4259. duration: textUtil.secToTime(thumbInfo.duration),
  4260. viewCounter: thumbInfo.viewCount,
  4261. mylistCounter: thumbInfo.mylistCount,
  4262. commentCounter: thumbInfo.commentCount,
  4263. description: thumbInfo.description,
  4264. lastResBody: thumbInfo.lastResBody,
  4265. isChannel,
  4266. ownerId: owner.id,
  4267. ownerName: owner.name,
  4268. ownerIcon: owner.icon,
  4269. tags: thumbInfo.tagList.map(tag => { return {text: tag.text, isLocked: tag.lock}; })
  4270. };
  4271.  
  4272. return new VideoInfo(rawData);
  4273. }
  4274.  
  4275. constructor(rawData) {
  4276. this._rawData = rawData;
  4277. }
  4278.  
  4279. get status() { return this._rawData.status; }
  4280. get videoId() { return this._rawData.videoId; }
  4281. get watchId() { return this._rawData.watchId; }
  4282. get originalVideoId() {
  4283. return (!this.isChannel && this.videoId !== this.watchId) ? this.videoId : '';
  4284. }
  4285. get videoTitle() { return this._rawData.videoTitle; }
  4286. get videoThumbnail() { return this._rawData.videoThumbnail; }
  4287. get description() { return this._rawData.description; }
  4288. get duration() { return this._rawData.duration; }
  4289. get owner() {
  4290. return {
  4291. type: this.isChannel ? 'channel' : 'user',
  4292. id: this.ownerId,
  4293. linkId: this.ownerId ? (this.isChannel ? `ch${this.ownerId}` : `user/${this.ownerId}`) : 'xx',
  4294. name: this.ownerName,
  4295. icon: this.ownerIcon
  4296. };
  4297. }
  4298.  
  4299. get ownerPageLink() {
  4300. const ownerId = this.ownerId;
  4301. if (this.isChannel) {
  4302. return `${protocol}//ch.nicovideo.jp/ch${ownerId}`;
  4303. } else {
  4304. return `${protocol}//www.nicovideo.jp/user/${ownerId}`;
  4305. }
  4306. }
  4307. get ownerIcon() { return this._rawData.ownerIcon; }
  4308. get ownerName() { return this._rawData.ownerName; }
  4309. get localeOwnerName() {
  4310. if (this.isChannel) {
  4311. return this.ownerName;
  4312. } else {
  4313. // TODO: 言語依存
  4314. return this.ownerName + ' さん';
  4315. }
  4316. }
  4317. get ownerId() { return this._rawData.ownerId; }
  4318. get isChannel() { return this._rawData.isChannel; }
  4319. get uploadDate() { return new Date(this._rawData.uploadDate); }
  4320.  
  4321. get viewCounter() { return this._rawData.viewCounter; }
  4322. get mylistCounter() { return this._rawData.mylistCounter; }
  4323. get commentCounter() { return this._rawData.commentCounter; }
  4324.  
  4325. get lastResBody() { return this._rawData.lastResBody; }
  4326. get tags() { return this._rawData.tags; }
  4327. }
  4328.  
  4329.  
  4330.  
  4331. const deflistAdd = (watchId) => {
  4332. const enableAutoComment = config.props.mylist.enableAutoComment;
  4333. if (location.host === 'www.nicovideo.jp') {
  4334. if (enableAutoComment) {
  4335. return DeflistApiLoader.addItemWithOwnerName(watchId);
  4336. } else {
  4337. return DeflistApiLoader.addItem(watchId, '');
  4338. }
  4339. }
  4340.  
  4341. let zenza;
  4342. let token;
  4343. return ZenzaDetector.detect().then((z) => {
  4344. zenza = z;
  4345. }).then(() => {
  4346. return CsrfTokenLoader.load().then((t) => {
  4347. token = t;
  4348. }, () => { return Promise.resolve(); });
  4349. }).then(() => {
  4350. if (!enableAutoComment) { return {}; }
  4351. return ThumbInfoLoader.loadOwnerInfo(watchId);
  4352. }).then((owner) => {
  4353. if (!owner.id) {
  4354. return zenza.external.deflistAdd({watchId});
  4355. }
  4356.  
  4357. const description = `${owner.localeName} ${owner.linkId}`;
  4358. return zenza.external.deflistAdd({watchId, description, token});
  4359. });
  4360. };
  4361.  
  4362. const deflistRemove = (watchId) => {
  4363. if (location.host === 'www.nicovideo.jp') {
  4364. return DeflistApiLoader.removeItem(watchId);
  4365. }
  4366.  
  4367. let zenza;
  4368. let token;
  4369. return ZenzaDetector.detect().then((z) => {
  4370. zenza = z;
  4371. }).then(() => {
  4372. return CsrfTokenLoader.load().then((t) => {
  4373. token = t;
  4374. }, () => { return Promise.resolve(); });
  4375. }).then(() => {
  4376. return zenza.external.deflistRemove({watchId, token});
  4377. });
  4378.  
  4379. };
  4380.  
  4381.  
  4382.  
  4383. class MatchChecker {
  4384. constructor({word = '', tag = '', owner = ''}) {
  4385. this.init({word, tag, owner});
  4386. }
  4387.  
  4388. init({word, tag, owner}) {
  4389. this._tag = [];
  4390. tag.split(/[\r\n]+/).forEach((t) => {
  4391. if (t) { this._tag.push(t.trim()); }
  4392. });
  4393. this._tag = _.uniq(this._tag);
  4394.  
  4395. let wordTmp = [];
  4396. this._word = null;
  4397. word.split(/[\r\n]+/).forEach((w) => {
  4398. if (w) { wordTmp.push(util.escapeRegs(w.trim())); }
  4399. });
  4400. wordTmp = _.uniq(wordTmp);
  4401. if (wordTmp.length > 0) {
  4402. this._word = new RegExp('(' + wordTmp.join('|') + ')', 'i');
  4403. }
  4404.  
  4405. this._userId = [];
  4406. this._channelId = [];
  4407. owner.split(/[\r\n]+/).forEach((o) => {
  4408. if (typeof o === 'string') {
  4409. const id = o.split('#')[0].trim();
  4410. if (id.startsWith('ch')) {
  4411. this._channelId.push(parseInt(id.substring(2)));
  4412. } else {
  4413. this._userId.push(parseInt(id));
  4414. }
  4415. }
  4416. });
  4417. this._userId = _.uniq(this._userId);
  4418. this._channelId = _.uniq(this._channelId);
  4419.  
  4420. }
  4421.  
  4422. isMatch(data) {
  4423. if (this._isMatchTag(data.tagList)) { return true; }
  4424. if (this._isMatchOwner(data.owner)) { return true; }
  4425. if (this._isMatchWord({title: data.title, description: data.description})) { return true; }
  4426. }
  4427.  
  4428. _isMatchTag(tagList = []) {
  4429. if (this._tag.length < 1) { return false; }
  4430.  
  4431. const tagTmp = [];
  4432. tagList.forEach(t => { if (t) { tagTmp.push(util.escapeRegs(t.trim ? t.trim() : t.text.trim())); } });
  4433. const tagReg = new RegExp(' (' + tagTmp.join('|') + ') ', 'i');
  4434. const _tag = ' ' + this._tag.join(' ') + ' ';
  4435. return tagReg.test(_tag);
  4436. }
  4437.  
  4438. _isMatchOwner(owner) {
  4439. const _id = owner.type === 'user' ? this._userId : this._channelId;
  4440. return _id.includes(parseInt(owner.id, 10));
  4441. }
  4442.  
  4443. _isMatchWord({title, description}) {
  4444. if (!this._word) { return false; }
  4445. return this._word.test(title) || this._word.test(description);
  4446. }
  4447.  
  4448. isMatchTag(tag) {
  4449. return this._isMatchTag([tag]);
  4450. }
  4451.  
  4452. isMatchOwner(owner) {
  4453. return this._isMatchOwner(owner);
  4454. }
  4455. }
  4456.  
  4457. class NgChecker extends MatchChecker {
  4458. isNg(data) {
  4459. return super.isMatch(data);
  4460. }
  4461. }
  4462.  
  4463. const initDom = () => {
  4464. util.addStyle(__css__);
  4465. const f = document.createElement('div');
  4466. f.id = 'mylistPocketDomContainer';
  4467. f.innerHTML = __tpl__;
  4468. document.body.appendChild(f);
  4469. };
  4470.  
  4471. const initZenzaBridge = () => {
  4472. ZenzaDetector.initialize();
  4473. };
  4474.  
  4475. const createVideoInfoView = () => {
  4476. const host = document.getElementById('mylistPocket-popup');
  4477. const tpl = document.getElementById('mylistPocket-popup-template');
  4478. const vv = new VideoInfoView({host, tpl});
  4479. return vv;
  4480. };
  4481.  
  4482. const createVideoInfoLoader = vv => {
  4483.  
  4484. const onVideoInfoLoad = thumbInfo => {
  4485. const vi = VideoInfo.createByThumbInfo(thumbInfo);
  4486. vv.bind(vi);
  4487. };
  4488.  
  4489. const onVideoInfoFail = () => {
  4490. vv.bind({status: 'fail', description: '通信失敗'});
  4491. return Promise.resolve();
  4492. };
  4493.  
  4494. return watchId => {
  4495. vv.reset();
  4496. vv.show();
  4497. return ThumbInfoLoader.load(watchId, {expireTime: 60 * 60 * 1000}).then(onVideoInfoLoad, onVideoInfoFail);
  4498. };
  4499. };
  4500.  
  4501. const createCommandDispatcher = ({infoView}) => {
  4502. const info = createVideoInfoLoader(infoView);
  4503.  
  4504. const ngConfig = config.namespace('ng');
  4505. const favConfig = config.namespace('fav');
  4506. const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
  4507.  
  4508. const toggleFavNg = (command, param) => {
  4509. let [cmd, namespace, key] = command.split('-');
  4510. let _config = namespace === 'fav' ? favConfig : ngConfig;
  4511. _config.refresh();
  4512. const value = param.value.trim();
  4513. let ngs = _config.props[key].trim().split(/[\r\n]/);
  4514. const isContain = ngs.includes(value);
  4515.  
  4516. if (isContain || cmd === 'remove') {
  4517. ngs = ngs.filter((line) => {
  4518. if (line === value) {
  4519. window.console.info('%c-%s:%s', 'background: cyan', key, value);
  4520. }
  4521. return line !== value;
  4522. });
  4523. cmd = 'remove';
  4524. } else if (!isContain || cmd === 'add') {
  4525. ngs.push(value);
  4526. window.console.info('%c+%s:%s', 'background: cyan', key, value);
  4527. cmd = 'add';
  4528. }
  4529.  
  4530. ngs = _.uniq(ngs);
  4531.  
  4532. _config.props[key] = ngs.join('\n').trim();
  4533.  
  4534. const className = namespace === 'fav' ? 'is-fav-favorited' : 'is-ng-rejected';
  4535. Array.prototype.forEach.call(
  4536. document.querySelectorAll(`*[data-watch-id=${param.watchId}]`),
  4537. item => { item.classList.toggle(className, cmd === 'add'); });
  4538. };
  4539.  
  4540. return (command, param, src) => {
  4541. switch(command) {
  4542. case 'info':
  4543. return info(param);
  4544. case 'load':
  4545. return QueueLoader.load(param);
  4546. case 'fav-status':
  4547. return QueueLoader.load(param).then((result) => {
  4548. if (!result || result.status === 'fail' || result.code === 'DELETED') {
  4549. return Promise.reject({status: 'unknown', result});
  4550. }
  4551. if (ngChecker.isMatch(result)) {
  4552. return {status: 'ng', result};
  4553. }
  4554. if (favChecker.isMatch(result)) {
  4555. return {status: 'favorite', result};
  4556. }
  4557. return {status: 'default', result};
  4558. });
  4559. case 'mylist-window':
  4560. window.open(
  4561. protocol + '//www.nicovideo.jp/mylist_add/video/' + param,
  4562. 'nicomylistadd',
  4563. 'width=500, height=400, menubar=no, scrollbars=no');
  4564. break;
  4565. case 'twitter-hash-open':
  4566. window.open('https://twitter.com/hashtag/' + param + '?src=hash');
  4567. break;
  4568. case 'open-mylist-open':
  4569. window.open(protocol + '//www.nicovideo.jp/openlist/' + param);
  4570. break;
  4571. case 'mylist-comment-open':
  4572. window.open(protocol + '//www.nicovideo.jp/mylistcomment/video/' + param);
  4573. break;
  4574. case 'zenza-open-now':
  4575. if (window.ZenzaWatch.config &&
  4576. window.ZenzaWatch.config.getValue('enableSingleton')) {
  4577. window.ZenzaWatch.external.sendOrExecCommand('openNow', param);
  4578. } else {
  4579. window.ZenzaWatch.external.execCommand('openNow', param);
  4580. }
  4581. break;
  4582. case 'zenza-open':
  4583. if (window.ZenzaWatch.config.getValue('enableSingleton')) {
  4584. window.ZenzaWatch.external.sendOrOpen(param);
  4585. } else {
  4586. window.ZenzaWatch.external.open(param);
  4587. }
  4588. break;
  4589. case 'playlist-inert':
  4590. window.ZenzaWatch.external.playlist.insert(param);
  4591. break;
  4592. case 'playlist-queue':
  4593. window.ZenzaWatch.external.playlist.add(param);
  4594. break;
  4595. case 'deflist-add':
  4596. src.notifyBeginDeflistUpdate('is-deflistUpdating');
  4597.  
  4598. return deflistAdd(param)
  4599. .then(util.getSleepPromise(1000, 'deflist-add'))
  4600. .then((result) => {
  4601. src.notifyEndDeflistUpdate(result);
  4602. }, (err) => {
  4603. console.error('deflist-add-result', err);
  4604. src.notifyFailDeflistUpdate(err);
  4605. });
  4606. case 'deflist-remove':
  4607. src.notifyBeginDeflistUpdate('is-deflistUpdating');
  4608.  
  4609. return deflistRemove(param)
  4610. .then(util.getSleepPromise(1000, 'deflist-remove'))
  4611. .then(() => {
  4612. src.notifyEndDeflistUpdate({message: '削除しました'});
  4613. }, (err) => {
  4614. console.error('deflist-remove-result', err);
  4615. src.notifyFailDeflistUpdate(err);
  4616. });
  4617. case 'add-ng-word': case 'add-ng-tag': case 'add-ng-owner':
  4618. case 'add-fav-word': case 'add-fav-tag': case 'add-fav-owner':
  4619. case 'remove-ng-word': case 'remove-ng-tag': case 'remove-ng-owner':
  4620. case 'remove-fav-word': case 'remove-fav-tag': case 'remove-fav-owner':
  4621. case 'toggle-ng-word': case 'toggle-ng-tag': case 'toggle-ng-owner':
  4622. case 'toggle-fav-word': case 'toggle-fav-tag': case 'toggle-fav-owner':
  4623. toggleFavNg(command, param);
  4624. break;
  4625. }
  4626. };
  4627. };
  4628.  
  4629. const initExternal = (dispatcher, hoverMenu, infoView) => {
  4630. MylistPocket.external = {
  4631. info: watchId => { return dispatcher('info', watchId); },
  4632. load: watchId => { return dispatcher('load', watchId, {expireTime: 60 * 60 * 1000}); },
  4633. getFavStatus: (watchId) => { return dispatcher('fav-status', watchId); },
  4634. observe: (params /*{query, container, closest}*/) => { initNg(params); },
  4635. hide: () => {
  4636. hoverMenu.hide();
  4637. infoView.hide();
  4638. }
  4639. };
  4640.  
  4641. MylistPocket.isReady = true;
  4642.  
  4643. const ev = new CustomEvent('MylistPocketInitialized', { detail: { MylistPocket } });
  4644. document.body.dispatchEvent(ev);
  4645. // 過去の互換用
  4646. if (window.jQuery) {
  4647. window.jQuery('body').trigger('MylistPocketReady', MylistPocket);
  4648. }
  4649. };
  4650.  
  4651.  
  4652. const QueueLoader = (() => {
  4653. let lastPromise = null;
  4654. let count = 0;
  4655. const MAX_LOAD = 6;
  4656. const promises = [];
  4657.  
  4658. const load = function(watchId, item) {
  4659. count = (count + 1) % MAX_LOAD;
  4660. lastPromise = promises[count];
  4661.  
  4662. const onLoad = info => {
  4663. if (item) {
  4664. watchId = info.watchId;
  4665. item.setAttribute('data-watch-id', watchId);
  4666. item.setAttribute('data-thumb-info', JSON.stringify(info));
  4667. }
  4668. const sleepTime = info.fromCache ? 0 : 50;
  4669. return (util.getSleepPromise(sleepTime, 'success-' + watchId))(info);
  4670. };
  4671. const onFail = util.getSleepPromise(1000, 'fail-' + watchId);
  4672.  
  4673. if (!lastPromise) {
  4674. if (item) { item.classList.add('is-ng-current'); }
  4675. lastPromise = ThumbInfoLoader.load(watchId).then(onLoad, onFail);
  4676. } else {
  4677. //lastPromise = Promise.all([lastPromise]).then(() => {
  4678. lastPromise = Promise.race(promises).then(() => {
  4679. if (item) { item.classList.add('is-ng-current'); }
  4680. return ThumbInfoLoader.load(watchId).then(onLoad, onFail);
  4681. });
  4682. }
  4683.  
  4684. promises[count] = lastPromise;
  4685. return lastPromise;
  4686. };
  4687.  
  4688. return {
  4689. load
  4690. };
  4691. })();
  4692.  
  4693. const getNgEnv = () => {
  4694. if (location.host === 'www.nicovideo.jp' &&
  4695. (location.pathname.startsWith('/ranking') ||
  4696. location.pathname.startsWith('/tag') ||
  4697. location.pathname.startsWith('/search'))
  4698. ) {
  4699. return {
  4700. query:
  4701. '.item[data-video-id]:not(.is-ng-wait), .item_cell[data-video-id]:not(.is-ng-wait), .VideoItem:not(.is-ng-wait), .RankingMainVideo[data-video-id]:not(.is-ng-wait)',
  4702. container:
  4703. Array.from(
  4704. document.querySelectorAll(
  4705. '.contentBody .list, .container.column1024-0, .RankingMatrixVideosRow, .RankingMainContainer, .RankingVideoListContainer')
  4706. )
  4707. };
  4708. }
  4709. if (location.host === 'www.nicovideo.jp' &&
  4710. document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp')) {
  4711. return {
  4712. query: '.NicorepoTimelineItem:not(.is-ng-wait)',
  4713. container: document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp'),
  4714. };
  4715. }
  4716.  
  4717. if (location.host === 'ch.nicovideo.jp' &&
  4718. location.pathname.startsWith('/search')) {
  4719. return {
  4720. query: '.item:not(.is-ng-wait)',
  4721. container: document.querySelector('.site_body')
  4722. };
  4723. }
  4724.  
  4725. if (location.host === 'search.nicovideo.jp') {
  4726. return {
  4727. query: '.video:not(.is-ng-wait)',
  4728. container: document.querySelector('#row-results')
  4729. };
  4730. }
  4731.  
  4732.  
  4733. return {query: null, container: null};
  4734. };
  4735.  
  4736. const initNgConfig = () => {
  4737. const ngConfig = config.namespace('ng');
  4738. const updateEnable = v => { document.body.classList.toggle('is-ng-disable', !v); };
  4739. updateEnable(ngConfig.props.enable);
  4740. if (!ngConfig.props.enable) { return {}; }
  4741. ngConfig.onkey('enable', updateEnable);
  4742.  
  4743. const favConfig = config.namespace('fav');
  4744. return {ngConfig, favConfig};
  4745. };
  4746.  
  4747. const initNgChecker = ({ngConfig, favConfig}) => {
  4748. const ngChecker = new NgChecker({
  4749. word: ngConfig.props.word,
  4750. tag: ngConfig.props.tag,
  4751. owner: ngConfig.props.owner
  4752. });
  4753.  
  4754. ngConfig.on('update', bounce.time(({key, value}) => {
  4755. ngChecker.init({
  4756. word: ngConfig.props.word,
  4757. tag: ngConfig.props.tag,
  4758. owner: ngConfig.props.owner
  4759. });
  4760. }, 100));
  4761.  
  4762.  
  4763. const favChecker = new MatchChecker({
  4764. word: favConfig.props.word,
  4765. tag: favConfig.props.tag,
  4766. owner: favConfig.props.owner
  4767. });
  4768.  
  4769. favConfig.on('update', bounce.time(({key, value}) => {
  4770. favChecker.init({
  4771. word: favConfig.props.word,
  4772. tag: favConfig.props.tag,
  4773. owner: favConfig.props.owner
  4774. });
  4775. }, 100));
  4776.  
  4777. return {ngChecker, favChecker};
  4778. };
  4779.  
  4780. const initIntersectionObserver = onInview => {
  4781.  
  4782. const onItemInview = item => {
  4783. let watchId = item.getAttribute('data-id') ||
  4784. item.getAttribute('data-video-id') ||
  4785. item.getAttribute('data-watch-id');
  4786. const ignore = () => item.classList.add('is-ng-ignore');
  4787. if (!watchId) {
  4788. const a = item.querySelector('a[href*=\'watch/\']');
  4789. let m;
  4790. if (!a) { return ignore(); }
  4791. if (a.hostname !== 'www.nicovideo.jp') { return ignore(); }
  4792. if ((m = /^\/watch\/([a-z0-9]+)/.exec(a.pathname)) === null) { return ignore(); }
  4793. watchId = m[1];
  4794. }
  4795.  
  4796. if (!watchId) { return ignore(); }
  4797.  
  4798. item.classList.add('is-ng-queue');
  4799. onInview(item, watchId);
  4800. };
  4801.  
  4802. const intersectionObserver = new window.IntersectionObserver(entries => {
  4803. entries.filter(entry => entry.isIntersecting).forEach(entry => {
  4804. const item = entry.target;
  4805. intersectionObserver.unobserve(item);
  4806. onItemInview(item);
  4807. });
  4808. }, { rootMargin: '400px'});
  4809.  
  4810. return intersectionObserver;
  4811. };
  4812.  
  4813. const initNgDom = ({intersectionObserver, query, closest, container}) => {
  4814.  
  4815. if (!container) { return; }
  4816. util.addStyle(__ng_css__);
  4817.  
  4818. const update = container => {
  4819. let items = (container || document).querySelectorAll(query);
  4820. if (!items || items.length < 1) { return; }
  4821. if (closest) {
  4822. let tmp = [];
  4823. Array.from(items).forEach(item => {
  4824. const c = item.closest(closest);
  4825. if (c && !tmp.includes(c)) {
  4826. tmp.push(c);
  4827. }
  4828. });
  4829. items = tmp;
  4830. }
  4831. if (!items || items.length < 1) { return; }
  4832. Array.from(items).forEach(item => {
  4833. //if (item.offsetLeft < 0) { return; }
  4834. if (item.classList.contains('is-ng-ignore')) { return; }
  4835. item.classList.add('is-ng-wait');
  4836. intersectionObserver.observe(item);
  4837. });
  4838. };
  4839. update();
  4840.  
  4841. const onUpdate = _.throttle(update, 1000);
  4842.  
  4843. if (!container) { return; }
  4844. const mutationObserver = new MutationObserver(mutations => {
  4845. if (mutations
  4846. .some(mutation => mutation.addedNodes && mutation.addedNodes.length)) {
  4847. onUpdate();
  4848. }
  4849. });
  4850.  
  4851. const containers = Array.isArray(container) ? container : [container];
  4852. containers.forEach(container => {
  4853. mutationObserver.observe(
  4854. container,
  4855. {childList: true, characterData: false, attributes: false, subtree: false}
  4856. );
  4857. });
  4858.  
  4859. };
  4860.  
  4861. const initNg = params => {
  4862. if (!window.IntersectionObserver) { return; }
  4863.  
  4864. let {query, container, closest} = params ? params : getNgEnv();
  4865.  
  4866. if (!query) { return; }
  4867.  
  4868. const {ngConfig, favConfig} = initNgConfig();
  4869. if (!ngConfig) { return; }
  4870.  
  4871. const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
  4872.  
  4873. const onItemInview = (item, watchId) => {
  4874.  
  4875. const loadLazy = () => {
  4876. const lazyImage = item.querySelector('.jsLazyImage');
  4877. if (lazyImage) {
  4878. const origImage = lazyImage.getAttribute('data-original');
  4879. if (origImage) {
  4880. lazyImage.src = origImage;
  4881. lazyImage.classList.remove('jsLazyImage');
  4882. }
  4883. }
  4884. };
  4885.  
  4886. QueueLoader.load(watchId, item).then(
  4887. info => {
  4888. item.classList.remove('is-ng-current');
  4889. if (!info || info.status === 'fail' || info.code === 'DELETED') {
  4890. if (info && info.code !== 'COMMUNITY') {
  4891. console.error('empty data', watchId, info, info ? info.code : 'unknown');
  4892. }
  4893. item.classList.add('is-ng-failed', info ? info.code : 'is-no-data');
  4894. } else {
  4895. item.classList.add(
  4896. ngChecker.isNg(info) ? 'is-ng-rejected' : 'is-ng-resolved');
  4897. if (favChecker.isMatch(info)) {
  4898. item.classList.add('is-fav-favorited');
  4899. }
  4900.  
  4901. for (let img of item.querySelectorAll('img.videoThumbnail.preview')) {
  4902. img.src = info.thumbnail;
  4903. }
  4904.  
  4905. let label = item.querySelector('.label');
  4906. item.dataset.title = info.title;
  4907. // チャンネル動画のリンクを watch/so〜 に置き換える
  4908. if (!(info.id || '').startsWith('so')) { return; }
  4909. if (label &&
  4910. item.classList.contains('videoLink')
  4911. ) {
  4912. label.textContent = info.id;
  4913. item.dataset.param = item.dataset.videoId = info.id;
  4914. item.href = `https://www.nicovideo.jp/watch/${info.id}`;
  4915. }
  4916. for (let a of item.querySelectorAll(`a[href*="watch/${watchId}"]`)) {
  4917. let href = a.getAttribute('href');
  4918. href = href.replace(/watch\/([0-9]+)/, `watch/${info.id}`);
  4919. a.setAttribute('href', href.replace(/^http:/, 'https:'));
  4920. }
  4921. }
  4922.  
  4923. loadLazy();
  4924. },
  4925. () => {
  4926. item.classList.remove('is-ng-current');
  4927. item.classList.add('is-ng-failed');
  4928. loadLazy();
  4929. }
  4930. );
  4931. };
  4932.  
  4933. const intersectionObserver = initIntersectionObserver(onItemInview);
  4934.  
  4935. initNgDom({intersectionObserver, query, container, closest});
  4936.  
  4937. return intersectionObserver;
  4938. };
  4939.  
  4940. const init = () => {
  4941. initDom();
  4942. initZenzaBridge();
  4943.  
  4944. const infoView = createVideoInfoView();
  4945. const dispatcher = createCommandDispatcher({infoView});
  4946.  
  4947. infoView.on('command', dispatcher);
  4948.  
  4949. const hoverMenu = new HoverMenu();
  4950. hoverMenu.on('info', (watchId) => {
  4951. hoverMenu.isBusy = true;
  4952.  
  4953. dispatcher('info', watchId)
  4954. .then(() => { hoverMenu.isBusy = false; });
  4955. });
  4956. hoverMenu.on('deflist-add', (watchId, src) => {
  4957. dispatcher('deflist-add', watchId, src);
  4958. });
  4959. hoverMenu.on('deflist-remove', (watchId, src) => {
  4960. dispatcher('deflist-remove', watchId, src);
  4961. });
  4962. hoverMenu.on('playlist-queue', (watchId, src) => {
  4963. dispatcher('playlist-queue', watchId, src);
  4964. });
  4965. MylistPocket.debug.hoverMenu = hoverMenu;
  4966.  
  4967. initNg();
  4968.  
  4969. if (config.props.nicoad.hide) {
  4970. util.addStyle(nicoadHideCss);
  4971. }
  4972. if (document.body.classList.contains('MatrixRanking-body') &&
  4973. config.props.responsive.matrix) {
  4974. util.addStyle(responsiveCss);
  4975. }
  4976.  
  4977. initExternal(dispatcher, hoverMenu, infoView);
  4978. };
  4979.  
  4980. init();
  4981. };
  4982. function EmitterInitFunc() {
  4983. class Handler { //extends Array {
  4984. constructor(...args) {
  4985. this._list = args;
  4986. }
  4987. get length() {
  4988. return this._list.length;
  4989. }
  4990. exec(...args) {
  4991. if (!this._list.length) {
  4992. return;
  4993. } else if (this._list.length === 1) {
  4994. this._list[0](...args);
  4995. return;
  4996. }
  4997. for (let i = this._list.length - 1; i >= 0; i--) {
  4998. this._list[i](...args);
  4999. }
  5000. }
  5001. execMethod(name, ...args) {
  5002. if (!this._list.length) {
  5003. return;
  5004. } else if (this._list.length === 1) {
  5005. this._list[0][name](...args);
  5006. return;
  5007. }
  5008. for (let i = this._list.length - 1; i >= 0; i--) {
  5009. this._list[i][name](...args);
  5010. }
  5011. }
  5012. add(member) {
  5013. if (this._list.includes(member)) {
  5014. return this;
  5015. }
  5016. this._list.unshift(member);
  5017. return this;
  5018. }
  5019. remove(member) {
  5020. this._list = this._list.filter(m => m !== member);
  5021. return this;
  5022. }
  5023. clear() {
  5024. this._list.length = 0;
  5025. return this;
  5026. }
  5027. get isEmpty() {
  5028. return this._list.length < 1;
  5029. }
  5030. *[Symbol.iterator]() {
  5031. const list = this._list || [];
  5032. for (const member of list) {
  5033. yield member;
  5034. }
  5035. }
  5036. next() {
  5037. return this[Symbol.iterator]();
  5038. }
  5039. }
  5040. Handler.nop = () => {/* ( ˘ω˘ ) スヤァ */};
  5041. const PromiseHandler = (() => {
  5042. const id = function() { return `Promise${this.id++}`; }.bind({id: 0});
  5043. class PromiseHandler extends Promise {
  5044. constructor(callback = () => {}) {
  5045. const key = new Object({id: id(), callback, status: 'pending'});
  5046. const cb = function(res, rej) {
  5047. const resolve = (...args) => { this.status = 'resolved'; this.value = args; res(...args); };
  5048. const reject = (...args) => { this.status = 'rejected'; this.value = args; rej(...args); };
  5049. if (this.result) {
  5050. return this.result.then(resolve, reject);
  5051. }
  5052. Object.assign(this, {resolve, reject});
  5053. return callback(resolve, reject);
  5054. }.bind(key);
  5055. super(cb);
  5056. this.resolve = this.resolve.bind(this);
  5057. this.reject = this.reject.bind(this);
  5058. this.key = key;
  5059. }
  5060. resolve(...args) {
  5061. if (this.key.resolve) {
  5062. this.key.resolve(...args);
  5063. } else {
  5064. this.key.result = Promise.resolve(...args);
  5065. }
  5066. return this;
  5067. }
  5068. reject(...args) {
  5069. if (this.key.reject) {
  5070. this.key.reject(...args);
  5071. } else {
  5072. this.key.result = Promise.reject(...args);
  5073. }
  5074. return this;
  5075. }
  5076. addCallback(callback) {
  5077. Promise.resolve().then(() => callback(this.resolve, this.reject));
  5078. return this;
  5079. }
  5080. }
  5081. return PromiseHandler;
  5082. })();
  5083. const {Emitter} = (() => {
  5084. let totalCount = 0;
  5085. let warnings = [];
  5086. class Emitter {
  5087. on(name, callback) {
  5088. if (!this._events) {
  5089. Emitter.totalCount++;
  5090. this._events = new Map();
  5091. }
  5092. name = name.toLowerCase();
  5093. let e = this._events.get(name);
  5094. if (!e) {
  5095. e = this._events.set(name, new Handler(callback));
  5096. } else {
  5097. e.add(callback);
  5098. }
  5099. if (e.length > 10) {
  5100. Emitter.warnings.push(this);
  5101. }
  5102. return this;
  5103. }
  5104. off(name, callback) {
  5105. if (!this._events) {
  5106. return;
  5107. }
  5108. name = name.toLowerCase();
  5109. const e = this._events.get(name);
  5110. if (!this._events.has(name)) {
  5111. return;
  5112. } else if (!callback) {
  5113. this._events.delete(name);
  5114. } else {
  5115. e.remove(callback);
  5116. if (e.isEmpty) {
  5117. this._events.delete(name);
  5118. }
  5119. }
  5120. if (this._events.size < 1) {
  5121. delete this._events;
  5122. }
  5123. return this;
  5124. }
  5125. once(name, func) {
  5126. const wrapper = (...args) => {
  5127. func(...args);
  5128. this.off(name, wrapper);
  5129. wrapper._original = null;
  5130. };
  5131. wrapper._original = func;
  5132. return this.on(name, wrapper);
  5133. }
  5134. clear(name) {
  5135. if (!this._events) {
  5136. return;
  5137. }
  5138. if (name) {
  5139. this._events.delete(name);
  5140. } else {
  5141. delete this._events;
  5142. Emitter.totalCount--;
  5143. }
  5144. return this;
  5145. }
  5146. emit(name, ...args) {
  5147. if (!this._events) {
  5148. return;
  5149. }
  5150. name = name.toLowerCase();
  5151. const e = this._events.get(name);
  5152. if (!e) {
  5153. return;
  5154. }
  5155. e.exec(...args);
  5156. return this;
  5157. }
  5158. emitAsync(...args) {
  5159. if (!this._events) {
  5160. return;
  5161. }
  5162. setTimeout(() => this.emit(...args), 0);
  5163. return this;
  5164. }
  5165. promise(name, callback) {
  5166. if (!this._promise) {
  5167. this._promise = {};
  5168. }
  5169. const p = this._promise[name];
  5170. if (p) {
  5171. return callback ? p.addCallback(callback) : p;
  5172. }
  5173. return this._promise[name] = new PromiseHandler(callback);
  5174. }
  5175. emitResolve(name, ...args) {
  5176. if (!this._promise) {
  5177. this._promise = {};
  5178. }
  5179. if (!this._promise[name]) {
  5180. this._promise[name] = new PromiseHandler();
  5181. }
  5182. this._promise[name].resolve(...args);
  5183. }
  5184. emitReject(name, ...args) {
  5185. if (!this._promise) {
  5186. this._promise = {};
  5187. }
  5188. if (!this._promise[name]) {
  5189. this._promise[name] = new PromiseHandler();
  5190. }
  5191. this._promise[name].reject(...args);
  5192. }
  5193. resetPromise(name) {
  5194. if (!this._promise) { return; }
  5195. delete this._promise[name];
  5196. }
  5197. hasPromise(name) {
  5198. return this._promise && !!this._promise[name];
  5199. }
  5200. }
  5201. Emitter.totalCount = totalCount;
  5202. Emitter.warnings = warnings;
  5203. return {Emitter};
  5204. })();
  5205. return {Handler, PromiseHandler, Emitter};
  5206. }
  5207. const {Handler, PromiseHandler, Emitter} = EmitterInitFunc();
  5208. function parseThumbInfo(xmlText) {
  5209. if (typeof xmlText !== 'string' || xmlText.status === 'ok') {
  5210. return xmlText;
  5211. }
  5212. const parser = new DOMParser();
  5213. const xml = parser.parseFromString(xmlText, 'text/xml');
  5214. const val = name => {
  5215. const elms = xml.getElementsByTagName(name);
  5216. if (elms.length < 1) {
  5217. return null;
  5218. }
  5219. return elms[0].textContent;
  5220. };
  5221. const dateToString = dateString => {
  5222. const date = new Date(dateString);
  5223. const [yy, mm, dd, h, m, s] = [
  5224. date.getFullYear(),
  5225. date.getMonth() + 1,
  5226. date.getDate(),
  5227. date.getHours(),
  5228. date.getMinutes(),
  5229. date.getSeconds()
  5230. ].map(n => n.toString().padStart(2, '0'));
  5231. return `${yy}/${mm}/${dd} ${h}:${m}:${s}`;
  5232. };
  5233. const resp = xml.getElementsByTagName('nicovideo_thumb_response');
  5234. if (resp.length < 1 || resp[0].getAttribute('status') !== 'ok') {
  5235. return {
  5236. status: 'fail',
  5237. code: val('code'),
  5238. message: val('description')
  5239. };
  5240. }
  5241. const [min, sec] = val('length').split(':');
  5242. const duration = min * 60 + sec * 1;
  5243. const watchId = val('watch_url').split('/').reverse()[0];
  5244. const postedAt = dateToString(new Date(val('first_retrieve')));
  5245. const tags = [...xml.getElementsByTagName('tag')].map(tag => {
  5246. return {
  5247. text: tag.textContent,
  5248. category: tag.hasAttribute('category'),
  5249. lock: tag.hasAttribute('lock')
  5250. };
  5251. });
  5252. const videoId = val('video_id');
  5253. const isChannel = videoId.substring(0, 2) === 'so';
  5254. const result = {
  5255. status: 'ok',
  5256. _format: 'thumbInfo',
  5257. v: isChannel ? videoId : watchId,
  5258. id: videoId,
  5259. videoId,
  5260. watchId: isChannel ? videoId : watchId,
  5261. originalVideoId: (!isChannel && watchId !== videoId) ? videoId : '',
  5262. isChannel,
  5263. title: val('title'),
  5264. description: val('description'),
  5265. thumbnail: val('thumbnail_url').replace(/^http:/, 'https:'),
  5266. movieType: val('movie_type'),
  5267. lastResBody: val('last_res_body'),
  5268. duration,
  5269. postedAt,
  5270. mylistCount: parseInt(val('mylist_counter'), 10),
  5271. viewCount: parseInt(val('view_counter'), 10),
  5272. commentCount: parseInt(val('comment_num'), 10),
  5273. tagList: tags
  5274. };
  5275. const userId = val('user_id');
  5276. if (userId !== null && userId !== '') {
  5277. result.owner = {
  5278. type: 'user',
  5279. id: userId,
  5280. linkId: userId ? `user/${userId}` : '',
  5281. name: val('user_nickname') || '(非公開ユーザー)',
  5282. url: userId ? ('https://www.nicovideo.jp/user/' + userId) : '#',
  5283. icon: val('user_icon_url') || 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg'
  5284. };
  5285. }
  5286. const channelId = val('ch_id');
  5287. if (channelId !== null && channelId !== '') {
  5288. result.owner = {
  5289. type: 'channel',
  5290. id: channelId,
  5291. linkId: channelId ? `ch${channelId}` : '',
  5292. name: val('ch_name') || '(非公開チャンネル)',
  5293. url: 'https://ch.nicovideo.jp/ch' + channelId,
  5294. icon: val('ch_icon_url') || 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg'
  5295. };
  5296. }
  5297. return result;
  5298. }
  5299. const workerUtil = (() => {
  5300. let config, TOKEN, PRODUCT = 'ZenzaWatch?', netUtil, CONSTANT, NAME = '';
  5301. let global = null, external = null;
  5302. const isAvailable = !!(window.Blob && window.Worker && window.URL);
  5303. const messageWrapper = function(self) {
  5304. const _onmessage = self.onmessage || (() => {});
  5305. const promises = {};
  5306. const onMessage = async function(self, type, e) {
  5307. const {body, sessionId, status} = e.data;
  5308. const {command, params} = body;
  5309. try {
  5310. let result;
  5311. switch (command) {
  5312. case 'commandResult':
  5313. if (promises[sessionId]) {
  5314. if (status === 'ok') {
  5315. if (params.command === 'fetch') {
  5316. const {buffer, init} = params.result;
  5317. promises[sessionId].resolve(new Response(buffer, init));
  5318. } else {
  5319. promises[sessionId].resolve(params.result);
  5320. }
  5321. } else {
  5322. promises[sessionId].reject(params.result);
  5323. }
  5324. delete promises[sessionId];
  5325. }
  5326. return;
  5327. case 'ping':
  5328. result = {now: Date.now(), NAME, PID, url: location.href};
  5329. break;
  5330. case 'port': {
  5331. const port = e.ports[0];
  5332. portMap[params.name] = port;
  5333. port.addEventListener('message', onMessage.bind({}, port, params.name));
  5334. bindFunc(port, 'MessageChannel');
  5335. if (params.ping) {
  5336. console.time('ping:' + sessionId);
  5337. port.ping().then(result => {
  5338. console.timeEnd('ping:' + sessionId);
  5339. console.log('ok %smec', Date.now() - params.now, params);
  5340. }).catch(err => {
  5341. console.timeEnd('ping:' + sessionId);
  5342. console.warn('ping fail', {err, data: e.data});
  5343. });
  5344. }
  5345. }
  5346. return;
  5347. case 'broadcast': {
  5348. if (!BroadcastChannel) { return; }
  5349. const channel = new BroadcastChannel(`${params.name}`);
  5350. channel.addEventListener('message', onMessage.bind({}, channel, 'BroadcastChannel'));
  5351. bindFunc(channel, 'BroadcastChannel');
  5352. bcast[params.basename] = channel;
  5353. }
  5354. return;
  5355. case 'env':
  5356. ({config, TOKEN, PRODUCT, CONSTANT} = params);
  5357. return;
  5358. default:
  5359. result = await _onmessage({command, params}, type, PID);
  5360. break;
  5361. }
  5362. self.postMessage({body:
  5363. {command: 'commandResult', params:
  5364. {command, result}}, sessionId, TYPE: type, PID, status: 'ok'
  5365. });
  5366. } catch(err) {
  5367. console.error('failed', {err, command, params, sessionId, TYPE: type, PID, data: e.data});
  5368. self.postMessage({body:
  5369. {command: 'commandResult', params: {command, result: err.message || null}},
  5370. sessionId, TYPE: type, PID, status: err.status || 'fail'
  5371. });
  5372. }
  5373. };
  5374. self.onmessage = onMessage.bind({}, self, self.name);
  5375. self.onconnect = e => {
  5376. const port = e.ports[0];
  5377. port.onmessage = self.onmessage;
  5378. port.start();
  5379. };
  5380. const bindFunc = (self, type = 'Worker') => {
  5381. const post = function(self, body, options = {}) {
  5382. const sessionId = `recv:${NAME}:${type}:${this.sessionId++}`;
  5383. return new Promise((resolve, reject) => {
  5384. promises[sessionId] = {resolve, reject};
  5385. self.postMessage({body, sessionId, PID}, options.transfer);
  5386. if (typeof options.timeout === 'number') {
  5387. setTimeout(() => {
  5388. reject({status: 'fail', message: 'timeout'});
  5389. delete promises[sessionId];
  5390. }, options.timeout);
  5391. }
  5392. }).finally(() => { delete promises[sessionId]; });
  5393. };
  5394. const emit = function(self, eventName, data = null) {
  5395. self.post({command: 'emit', params: {eventName, data}});
  5396. };
  5397. const notify = function(self, message) {
  5398. self.post({command: 'notify', params: {message}});
  5399. };
  5400. const alert = function(self, message) {
  5401. self.post({command: 'alert', params: {message}});
  5402. };
  5403. const ping = async function(self, options = {}) {
  5404. const timekey = `PING "${self.name}"`;
  5405. console.log(timekey);
  5406. let result;
  5407. options.timeout = options.timeout || 10000;
  5408. try {
  5409. console.time(timekey);
  5410. result = await self.post({command: 'ping', params: {now: Date.now(), NAME, PID, url: location.href}}, options);
  5411. console.timeEnd(timekey);
  5412. } catch (e) {
  5413. console.timeEnd(timekey);
  5414. console.warn('ping fail', e);
  5415. }
  5416. return result;
  5417. };
  5418. self.post = post.bind({sessionId: 0}, this.port || self);
  5419. self.emit = emit.bind({}, self);
  5420. self.notify = notify.bind({}, self);
  5421. self.alert = alert.bind({}, self);
  5422. self.ping = ping.bind({}, self);
  5423. return self;
  5424. };
  5425. bindFunc(self);
  5426. self.xFetch = (url, options = {}) => {
  5427. options = {...options, ...{signal: null}}; // remove AbortController
  5428. if (url.startsWith(location.origin)) {
  5429. return fetch(url, options);
  5430. }
  5431. return self.post({command: 'fetch', params: {url, options}});
  5432. };
  5433. };
  5434. const workerUtil = {
  5435. isAvailable,
  5436. js: (q, ...args) => {
  5437. const strargs = args.map(a => typeof a === 'string' ? a : a.toString);
  5438. return String.raw(q, ...strargs);
  5439. },
  5440. env: params => {
  5441. ({config, TOKEN, PRODUCT, netUtil, CONSTANT} =
  5442. Object.assign({config, TOKEN, PRODUCT, netUtil, CONSTANT}, params));
  5443. if (global) { ({config, TOKEN, PRODUCT, netUtil, CONSTANT} = global); }
  5444. },
  5445. create: function(func, options = {}) {
  5446. let cache = this.urlMap.get(func);
  5447. const name = options.name || 'Worker';
  5448. if (!cache) {
  5449. const src = `
  5450. const PID = '${window && window.name || 'self'}:${location.href}:${name}:${Date.now().toString(16).toUpperCase()}';
  5451. console.log('%cinit "%s"', 'font-weight: bold;', self.name || '');
  5452. (${func.toString()})(self);
  5453. `;
  5454. const blob = new Blob([src], {type: 'text/javascript'});
  5455. const url = URL.createObjectURL(blob);
  5456. this.urlMap.set(func, url);
  5457. cache = url;
  5458. }
  5459. if (options.type === 'SharedWorker') {
  5460. const w = this.workerMap.get(func) || new SharedWorker(cache);
  5461. this.workerMap.set(func, w);
  5462. return w;
  5463. }
  5464. return new Worker(cache, options);
  5465. }.bind({urlMap: new Map(), workerMap: new Map()}),
  5466. createCrossMessageWorker: function(func, options = {}) {
  5467. const promises = this.promises;
  5468. const name = options.name || 'Worker';
  5469. const PID = `${window && window.name || 'self'}:${location.host}:${name}:${Date.now().toString(16).toUpperCase()}`;
  5470. const _func = `
  5471. function (self) {
  5472. let config = {}, PRODUCT, TOKEN, CONSTANT, NAME = decodeURI('${encodeURI(name)}'), bcast = {}, portMap = {};
  5473. const {Handler, PromiseHandler, Emitter} = (${EmitterInitFunc.toString()})();
  5474. (${func.toString()})(self);
  5475. //===================================
  5476. (${messageWrapper.toString()})(self);
  5477. }
  5478. `;
  5479. const worker = workerUtil.create(_func, options);
  5480. const self = options.type === 'SharedWorker' ? worker.port : worker;
  5481. self.name = name;
  5482. const onMessage = async function(self, e) {
  5483. const {body, sessionId, status} = e.data;
  5484. const {command, params} = body;
  5485. try {
  5486. let result = 'ok';
  5487. let transfer = null;
  5488. switch (command) {
  5489. case 'commandResult':
  5490. if (promises[sessionId]) {
  5491. if (status === 'ok') {
  5492. promises[sessionId].resolve(params.result);
  5493. } else {
  5494. promises[sessionId].reject(params.result);
  5495. }
  5496. delete promises[sessionId];
  5497. }
  5498. return;
  5499. case 'ping':
  5500. result = {now: Date.now(), NAME, PID, url: location.href};
  5501. console.timeLog && console.timeLog(params.NAME, 'PONG');
  5502. break;
  5503. case 'emit':
  5504. global && global.emitter.emitAsync(params.eventName, params.data);
  5505. break;
  5506. case 'fetch':
  5507. result = (netUtil || window).fetch(params.url,
  5508. Object.assign({}, params.options || {}, {_format: 'arraybuffer'}));
  5509. transfer = [result.buffer];
  5510. break;
  5511. case 'notify':
  5512. global && global.notify(params.message);
  5513. break;
  5514. case 'alert':
  5515. global && global.alert(params.message);
  5516. break;
  5517. default:
  5518. self.oncommand && (result = await self.oncommand({command, params}));
  5519. break;
  5520. }
  5521. self.postMessage({body: {command: 'commandResult', params: {command, result}}, sessionId, status: 'ok'}, transfer);
  5522. } catch (err) {
  5523. console.error('failed', {err, command, params, sessionId});
  5524. self.postMessage({body: {command: 'commandResult', params: {command, result: err.message || null}}, sessionId, status: err.status || 'fail'});
  5525. }
  5526. };
  5527. const bindFunc = (self, type = 'Worker') => {
  5528. const post = function(self, body, options = {}) {
  5529. const sessionId = `send:${name}:${type}:${this.sessionId++}`;
  5530. return new Promise((resolve, reject) => {
  5531. promises[sessionId] = {resolve, reject};
  5532. self.postMessage({body, sessionId, TYPE: type, PID}, options.transfer);
  5533. if (typeof options.timeout === 'number') {
  5534. setTimeout(() => {
  5535. reject({status: 'fail', message: 'timeout'});
  5536. delete promises[sessionId];
  5537. }, options.timeout);
  5538. }
  5539. }).finally(() => { delete promises[sessionId]; });
  5540. };
  5541. const ping = async function(self, options = {}) {
  5542. const timekey = `PING "${self.name}" total time`;
  5543. window.console.log(`PING "${self.name}"...`);
  5544. let result;
  5545. options.timeout = options.timeout || 10000;
  5546. try {
  5547. window.console.time(timekey);
  5548. result = await self.post({command: 'ping', params: {now: Date.now(), NAME: self.name, PID, url: location.href}}, options);
  5549. window.console.timeEnd(timekey);
  5550. } catch (e) {
  5551. console.timeEnd(timekey);
  5552. console.warn('ping fail', e);
  5553. }
  5554. return result;
  5555. };
  5556. self.post = post.bind({sessionId: 0}, self);
  5557. self.ping = ping.bind({}, self);
  5558. self.addEventListener('message', onMessage.bind({sessionId: 0}, self));
  5559. self.start && self.start();
  5560. };
  5561. bindFunc(self);
  5562. if (config) {
  5563. self.post({
  5564. command: 'env',
  5565. params: {config: config.export(true), TOKEN, PRODUCT, CONSTANT}
  5566. });
  5567. }
  5568. self.addPort = (port, options = {}) => {
  5569. const name = options.name || 'MessageChannel';
  5570. return self.post({command: 'port', params: {port, name}}, {transfer: [port]});
  5571. };
  5572. const channel = new MessageChannel();
  5573. self.addPort(channel.port2);
  5574. bindFunc(channel.port1, {name: 'MessageChannel'});
  5575. self.bridge = async (worker, options = {}) => {
  5576. const name = options.name || 'MessageChannelBridge';
  5577. const channel = new MessageChannel();
  5578. await self.addPort(channel.port1, {name: worker.name || name});
  5579. await worker.addPort(channel.port2, {name: self.name || name});
  5580. console.log('ping self -> other', await channel.port1.ping());
  5581. console.log('ping other -> self', await channel.port2.ping());
  5582. };
  5583. self.BroadcastChannel = basename => {
  5584. const name = `${basename || 'Broadcast'}${TOKEN || Date.now().toString(16)}`;
  5585. self.post({command: 'broadcast', params: {basename, name}});
  5586. const channel = new BroadcastChannel(name);
  5587. channel.addEventListener('message', onMessage.bind({}, channel, 'BroadcastChannel'));
  5588. bindFunc(channel, 'BroadcastChannel');
  5589. return name;
  5590. };
  5591. self.ping()
  5592. .then(result => window.console.log('OK'))
  5593. .catch(result => console.warn('FAIL', result));
  5594. return self;
  5595. }.bind({
  5596. sessionId: 0,
  5597. promises: {}
  5598. })
  5599. };
  5600. return workerUtil;
  5601. })();
  5602. const IndexedDbStorage = (() => {
  5603. const workerFunc = function(self) {
  5604. const db = {};
  5605. const controller = {
  5606. async init({name, ver, stores}) {
  5607. if (db[name]) {
  5608. return Promise.resolve(db[name]);
  5609. }
  5610. return new Promise((resolve, reject) => {
  5611. const req = indexedDB.open(name, ver);
  5612. req.onupgradeneeded = e => {
  5613. const _db = e.target.result;
  5614. for (const meta of stores) {
  5615. if(_db.objectStoreNames.contains(meta.name)) {
  5616. _db.deleteObjectStore(meta.name);
  5617. }
  5618. const store = _db.createObjectStore(meta.name, meta.definition);
  5619. const indexes = meta.indexes || [];
  5620. for (const idx of indexes) {
  5621. store.createIndex(idx.name, idx.keyPath, idx.params);
  5622. }
  5623. store.transaction.oncomplete = () => {
  5624. console.log('store.transaction.complete', JSON.stringify({name, ver, store: meta}));
  5625. };
  5626. }
  5627. };
  5628. req.onsuccess = e => {
  5629. db[name] = e.target.result;
  5630. resolve(db[name]);
  5631. };
  5632. req.onerror = reject;
  5633. });
  5634. },
  5635. close({name}) {
  5636. if (!db[name]) {
  5637. return;
  5638. }
  5639. db[name].close();
  5640. db[name] = null;
  5641. },
  5642. async getStore({name, storeName, mode = 'readonly'}) {
  5643. const db = await this.init({name});
  5644. return new Promise(async (resolve, reject) => {
  5645. const tx = db.transaction(storeName, mode);
  5646. tx.onerror = reject;
  5647. return resolve({
  5648. store: tx.objectStore(storeName),
  5649. transaction: tx
  5650. });
  5651. });
  5652. },
  5653. async put({name, storeName, data}) {
  5654. const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
  5655. return new Promise((resolve, reject) => {
  5656. const req = store.put(data);
  5657. req.onsuccess = e => {
  5658. transaction.commit();
  5659. resolve(e.target.result);
  5660. };
  5661. req.onerror = reject;
  5662. });
  5663. },
  5664. async get({name, storeName, data: {key, index, timeout}}) {
  5665. const {store} = await this.getStore({name, storeName});
  5666. return new Promise((resolve, reject) => {
  5667. const req =
  5668. index ?
  5669. store.index(index).get(key) : store.get(key);
  5670. req.onsuccess = e => resolve(e.target.result);
  5671. req.onerror = reject;
  5672. if (timeout) {
  5673. setTimeout(() => {
  5674. reject(`timeout: key${key}`);
  5675. }, timeout);
  5676. }
  5677. });
  5678. },
  5679. async updateTime({name, storeName, data: {key, index, timeout}}) {
  5680. const record = await this.get({name, storeName, data: {key, index, timeout}});
  5681. if (!record) {
  5682. return null;
  5683. }
  5684. record.updatedAt = Date.now();
  5685. this.put({name, storeName, data: record});
  5686. return record;
  5687. },
  5688. async delete({name, storeName, data: {key, index}}) {
  5689. const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
  5690. return new Promise((resolve, reject) => {
  5691. let remove = 0;
  5692. let range = IDBKeyRange.only(key);
  5693. let req =
  5694. index ?
  5695. store.index(index).openCursor(range) : store.openCursor(range);
  5696. req.onsuccess = e => {
  5697. const result = e.target.result;
  5698. if (!result) {
  5699. transaction.commit();
  5700. return resolve(remove > 0);
  5701. }
  5702. result.delete();
  5703. remove++;
  5704. result.continue();
  5705. };
  5706. req.onerror = reject;
  5707. });
  5708. },
  5709. async clear({name, storeName}) {
  5710. const {store} = await this.getStore({name, storeName, mode: 'readwrite'});
  5711. return new Promise((resolve, reject) => {
  5712. const req = store.clear();
  5713. req.onsuccess = e => {
  5714. console.timeEnd('storage clear');
  5715. resolve();
  5716. };
  5717. req.onerror = e => {
  5718. console.timeEnd('storage clear');
  5719. reject(e);
  5720. };
  5721. });
  5722. },
  5723. async gc({name, storeName, data: {expireTime}}) {
  5724. const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
  5725. const now = Date.now(), ptime = performance.now();
  5726. const expiresAt = now - expireTime;
  5727. const expireDateTime = new Date(expiresAt).toLocaleString();
  5728. const timekey = `GC [DELETE FROM ${name}.${storeName} WHERE updatedAt < '${expireDateTime}'] `;
  5729. console.time(timekey);
  5730. let count = 0;
  5731. return new Promise((resolve, reject) => {
  5732. const range = IDBKeyRange.upperBound(expiresAt);
  5733. const idx = store.index('updatedAt');
  5734. const req = idx.openCursor(range);
  5735. req.onsuccess = e => {
  5736. const cursor = e.target.result;
  5737. if (cursor) {
  5738. count++;
  5739. cursor.delete();
  5740. return cursor.continue();
  5741. }
  5742. console.timeEnd(timekey);
  5743. resolve({status: 'ok', count, time: performance.now() - ptime});
  5744. };
  5745. req.onerror = reject;
  5746. }).catch((e) => {
  5747. console.error('gc fail', e);
  5748. store.clear();
  5749. });
  5750. }
  5751. };
  5752. const d2a = async dataUrl => fetch(dataUrl).then(r => r.arrayBuffer());
  5753. const a2d = async (arrayBuffer, type = 'image/jpeg') => {
  5754. return new Promise((ok, ng) => {
  5755. const reader = new FileReader();
  5756. reader.onload = () => ok(reader.result);
  5757. reader.onerror = ng;
  5758. reader.readAsDataURL(new Blob([arrayBuffer], {type}));
  5759. });
  5760. };
  5761. self.onmessage = async ({command, params}) => {
  5762. try {
  5763. switch (command) {
  5764. case 'init':
  5765. await controller[command](params);
  5766. return 'ok';
  5767. case 'put': {
  5768. const {name, storeName, data} = params;
  5769. if (data.dataUrls) { // dataURLのままだと肥大化するのでArrayBufferにする
  5770. data.dataUrls = await Promise.all(data.dataUrls.map(url => d2a(url)));
  5771. }
  5772. return controller.put({name, storeName, data});
  5773. }
  5774. case 'updateTime':
  5775. case 'get': {
  5776. const data = await controller[command](params);
  5777. if (data && data.dataUrls) {
  5778. data.dataUrls = await Promise.all(data.dataUrls.map(url => a2d(url)));
  5779. }
  5780. return data;
  5781. }
  5782. default:
  5783. return controller[command](params) || 'ok';
  5784. }
  5785. } catch (err) {
  5786. console.warn('command failed: ', {command, params});
  5787. throw err;
  5788. }
  5789. };
  5790. return controller;
  5791. };
  5792. const workers = {};
  5793. const open = async ({name, ver, stores}, func) => {
  5794. if (!workers[name]) {
  5795. let _func = workerFunc;
  5796. if (func) {
  5797. _func = `
  5798. (() => {
  5799. const controller = (${workerFunc.toString()})(self);
  5800. (${func.toString()})(self)
  5801. })
  5802. `;
  5803. }
  5804. workers[name] = workerUtil.createCrossMessageWorker(_func, {name: `IndexedDb[${name}]`});
  5805. }
  5806. const worker = workers[name];
  5807. worker.post({command: 'init', params: {name, ver, stores}});
  5808. const post = (command, data, storeName, transfer) => {
  5809. const params = {data, name, storeName, transfer};
  5810. return worker.post({command, params}, transfer);
  5811. };
  5812. const result = {worker};
  5813. for (const meta of stores) {
  5814. const storeName = meta.name;
  5815. result[storeName] = (storeName => {
  5816. return {
  5817. close: params => post('close', params, storeName),
  5818. put: (record, transfer) => post('put', record, storeName, transfer),
  5819. get: ({key, index, timeout}) => post('get', {key, index, timeout}, storeName),
  5820. updateTime: ({key, index, timeout}) => post('updateTime', {key, index, timeout}, storeName),
  5821. delete: ({key, index, timeout}) => post('delete', {key, index, timeout}, storeName),
  5822. gc: (expireTime = 30 * 24 * 60 * 60 * 1000) => post('gc', {expireTime}, storeName)
  5823. };
  5824. })(storeName);
  5825. }
  5826. return result;
  5827. };
  5828. return {open};
  5829. })();
  5830. const ThumbInfoCacheDb = (() => {
  5831. const THUMB_INFO = {
  5832. name: 'thumb-info',
  5833. ver: 1,
  5834. stores: [
  5835. {
  5836. name: 'cache',
  5837. indexes: [
  5838. {name: 'postedAt', keyPath: 'postedAt', params: {unique: false}},
  5839. {name: 'updatedAt', keyPath: 'updatedAt', params: {unique: false}}
  5840. ],
  5841. definition: {keyPath: 'watchId', autoIncrement: false}
  5842. }
  5843. ]
  5844. };
  5845. let db;
  5846. const open = async () => {
  5847. db = db || await IndexedDbStorage.open(THUMB_INFO);
  5848. const cacheDb = db['cache'];
  5849. cacheDb.gc(90 * 24 * 60 * 60 * 1000);
  5850. return {
  5851. put: (xml, thumbInfo = null) => {
  5852. thumbInfo = thumbInfo || parseThumbInfo(xml);
  5853. if (thumbInfo.status !== 'ok') {
  5854. return;
  5855. }
  5856. const watchId = thumbInfo.v;
  5857. const videoId = thumbInfo.id;
  5858. const postedAt = new Date(thumbInfo.postedAt).getTime();
  5859. const updatedAt = Date.now();
  5860. const record = {
  5861. watchId,
  5862. videoId,
  5863. postedAt,
  5864. updatedAt,
  5865. xml,
  5866. thumbInfo
  5867. };
  5868. cacheDb.put(record);
  5869. return {watchId, updatedAt};
  5870. },
  5871. get: watchId => cacheDb.updateTime({key: watchId}),
  5872. delete: watchId => cacheDb.delete({key: watchId}),
  5873. close: () => cacheDb.close()
  5874. };
  5875. };
  5876. return {open};
  5877. })();
  5878. window.MylistPocketLib = {
  5879. workerUtil
  5880. };
  5881. const thumbInfoApi = async function() {
  5882. const gate = () => {
  5883. const post = function(body, {type, token, sessionId, origin} = {}) {
  5884. sessionId = sessionId || '';
  5885. origin = origin || '';
  5886. this.origin = origin = origin || this.origin || document.referrer;
  5887. this.token = token = token || this.token;
  5888. this.type = type = type || this.type;
  5889. if (!this.channel) {
  5890. this.channel = new MessageChannel;
  5891. }
  5892. const url = location.href;
  5893. const id = PRODUCT;
  5894. try {
  5895. const msg = {id, type, token, url, sessionId, body};
  5896. if (!this.port) {
  5897. msg.body = {command: 'initialized', params: msg.body};
  5898. parent.postMessage(msg, origin, [this.channel.port2]);
  5899. this.port = this.channel.port1;
  5900. this.port.start();
  5901. } else {
  5902. this.port.postMessage(msg);
  5903. }
  5904. } catch (e) {
  5905. console.error('%cError: parent.postMessage - ', 'color: red; background: yellow', e);
  5906. }
  5907. return this.port;
  5908. }.bind({channel: null, port: null, origin: null, token: null, type: null});
  5909. const parseUrl = url => {
  5910. url = url || 'https://unknown.example.com/';
  5911. const a = document.createElement('a');
  5912. a.href = url;
  5913. return a;
  5914. };
  5915. const isNicoServiceHost = url => {
  5916. const host = parseUrl(url).hostname;
  5917. return /(^[a-z0-9.-]*\.nicovideo\.jp$|^[a-z0-9.-]*\.nico(|:[0-9]+)$)/.test(host);
  5918. };
  5919. const isWhiteHost = url => {
  5920. const u = parseUrl(url);
  5921. const host = u.hostname;
  5922. if (['account.nicovideo.jp', 'point.nicovideo.jp'].includes(host)) {
  5923. return false;
  5924. }
  5925. if (isNicoServiceHost(url)) {
  5926. return true;
  5927. }
  5928. if (['localhost', '127.0.0.1'].includes(host)) { return true; }
  5929. if (localStorage.ZenzaWatch_whiteHost) {
  5930. if (localStorage.ZenzaWatch_whiteHost.split(',').includes(host)) {
  5931. return true;
  5932. }
  5933. }
  5934. if (u.protocol !== 'https:') { return false; }
  5935. return [
  5936. 'google.com',
  5937. 'www.google.com',
  5938. 'www.google.co.jp',
  5939. 'www.bing.com',
  5940. 'twitter.com',
  5941. 'friends.nico',
  5942. 'feedly.com',
  5943. 'www.youtube.com',
  5944. ].includes(host) || host.endsWith('.slack.com');
  5945. };
  5946. const uFetch = params => {
  5947. const {url, options}= params;
  5948. if (!isWhiteHost(url) || !isNicoServiceHost(url)) {
  5949. return Promise.reject({status: 'fail', message: 'network error'});
  5950. }
  5951. const racers = [];
  5952. let timer;
  5953. const timeout = (typeof params.timeout === 'number' && !isNaN(params.timeout)) ? params.timeout : 30 * 1000;
  5954. if (timeout > 0) {
  5955. racers.push(new Promise((resolve, reject) =>
  5956. timer = setTimeout(() => timer ? reject({name: 'timeout', message: 'timeout'}) : resolve(), timeout))
  5957. );
  5958. }
  5959. const controller = AbortController ? (new AbortController()) : null;
  5960. if (controller) {
  5961. params.signal = controller.signal;
  5962. }
  5963. racers.push(fetch(url, options));
  5964. return Promise.race(racers)
  5965. .catch(err => {
  5966. let message = 'uFetch fail';
  5967. if (err && err.name === 'timeout') {
  5968. if (controller) {
  5969. console.warn('request timeout');
  5970. controller.abort();
  5971. }
  5972. message = 'timeout';
  5973. }
  5974. return Promise.reject({status: 'fail', message});
  5975. }).finally(() => { timer && clearTimeout(timer); });
  5976. };
  5977. const xFetch = (params, sessionId = null) => {
  5978. const command = 'fetch';
  5979. return uFetch(params).then(async resp => {
  5980. const buffer = await resp.arrayBuffer();
  5981. const init = ['type', 'url', 'redirected', 'status', 'ok', 'statusText']
  5982. .reduce((map, key) => {map[key] = resp[key]; return map;}, {});
  5983. const headers = [...resp.headers.entries()];
  5984. return Promise.resolve({buffer, init, headers});
  5985. }).then(({buffer, init, headers}) => {
  5986. const result = {status: 'ok', command, params: {buffer, init, headers}};
  5987. post(result, {sessionId});
  5988. return result;
  5989. }).catch(({status, message}) => {
  5990. post({status, message, command}, {sessionId});
  5991. });
  5992. };
  5993. const init = ({prefix, type}) => {
  5994. if (!window.name.startsWith(prefix)) {
  5995. throw new Error(`unknown name "${window.name}"`);
  5996. }
  5997. const PID = `${window && window.name || 'self'}:${location.host}:${name}:${Date.now().toString(16).toUpperCase()}`;
  5998. type = type || window.name.replace(new RegExp(`/(${PRODUCT}|)Loader$/`), '');
  5999. const origin = document.referrer || window.name.split('#')[1];
  6000. console.log('%cCrossDomainGate: %s', 'background: lightgreen;', location.host, window.name, {prefix, type});
  6001. if (!isWhiteHost(origin)) {
  6002. throw new Error(`disable bridge "${origin}"`);
  6003. }
  6004. const TOKEN = location.hash ? location.hash.substring(1) : null;
  6005. window.history.replaceState(null, null, location.pathname);
  6006. const port = post({status: 'ok', command: 'initialized'}, {type, token: TOKEN, origin});
  6007. workerUtil && workerUtil.env({TOKEN, PRODUCT});
  6008. return {port, TOKEN, origin, type, PID};
  6009. };
  6010. return {post, parseUrl, isNicoServiceHost, isWhiteHost, uFetch, xFetch, init};
  6011. };
  6012. const {post, parseUrl, uFetch, init} = gate();
  6013. const {port, TOKEN} = init({prefix: `thumbInfo${PRODUCT}`, type: 'thumbInfo'});
  6014. const db = await ThumbInfoCacheDb.open();
  6015. port.addEventListener('message', async e => {
  6016. const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
  6017. const {body, sessionId, token} = data;
  6018. const {command, params} = body;
  6019. if (command !== 'fetch') { return; }
  6020. const p = parseUrl(params.url);
  6021. if (TOKEN !== token ||
  6022. p.hostname !== location.host ||
  6023. !p.pathname.startsWith('/api/getthumbinfo/')) {
  6024. console.log('invalid msg: ', {origin: e.origin, TOKEN, token, body});
  6025. return;
  6026. }
  6027. params.options = params.options || {};
  6028.  
  6029. const watchId = params.url.split('/').reverse()[0];
  6030. const expiresAt = Date.now() - (params.options.expireTime || 0);
  6031. const cache = await db.get(watchId);
  6032. if (cache && cache.thumbInfo.status === 'ok' && cache.updatedAt < expiresAt) {
  6033. return post({status: 'ok', command, params: cache.thumbInfo}, {sessionId});
  6034. }
  6035.  
  6036.  
  6037. delete params.options.credentials;
  6038. return uFetch(params, sessionId)
  6039. .then(res => res.text())
  6040. .then(async xmlText => {
  6041. let thumbInfo = parseThumbInfo(xmlText);
  6042. if (thumbInfo.status === 'ok') {
  6043. db.put(xmlText, thumbInfo);
  6044. } else if (cache && cache.thumbInfo.status === 'ok') {
  6045. thumbInfo = cache.thumbInfo;
  6046. }
  6047. const result = {status: 'ok', command, params: thumbInfo};
  6048. post(result, {sessionId});
  6049. }).catch(({status, message}) => {
  6050. if (cache && cache.thumbInfo.status === 'ok') {
  6051. return post({status: 'ok', command, params: cache.thumbInfo}, {sessionId});
  6052. }
  6053. return post({status, message, command}, {sessionId});
  6054. });
  6055. });
  6056. };
  6057.  
  6058. const loadGm = () => {
  6059. const script = document.createElement('script');
  6060. script.id = `${PRODUCT}Loader`;
  6061. script.setAttribute('type', 'text/javascript');
  6062. script.setAttribute('charset', 'UTF-8');
  6063. script.append(`
  6064. (() => {
  6065. const {Handler, PromiseHandler, Emitter} = (${EmitterInitFunc.toString()})();
  6066. ${parseThumbInfo.toString()}
  6067.  
  6068. (${monkey.toString()})("${PRODUCT}");
  6069. })();`);
  6070. (document.head || document.documentElement).append(script);
  6071. };
  6072.  
  6073. const host = window.location.host || '';
  6074. if (host === 'ext.nicovideo.jp' &&
  6075. window.name.indexOf(`thumbInfo${PRODUCT}Loader`) >= 0) {
  6076. thumbInfoApi();
  6077. } else if (window === top) {
  6078. loadGm();
  6079. }
  6080. });