MylistPocket

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

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

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