MylistPocket

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

当前为 2016-11-23 提交的版本,查看 最新版本

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