MylistPocket

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

目前为 2016-11-29 提交的版本,查看 最新版本

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