// ==UserScript== // @name MylistPocket // @namespace https://github.com/segabito/ // @description 動画をあとで見る + 簡易NG機能。 ZenzaWatchとの連携も可能。 // @match *://www.nicovideo.jp/* // @match *://ext.nicovideo.jp/ // @match *://ext.nicovideo.jp/#* // @match *://ch.nicovideo.jp/* // @match *://com.nicovideo.jp/* // @match *://commons.nicovideo.jp/* // @match *://dic.nicovideo.jp/* // @match *://ex.nicovideo.jp/* // @match *://info.nicovideo.jp/* // @match *://search.nicovideo.jp/* // @match *://uad.nicovideo.jp/* // @match *://site.nicovideo.jp/* // @match *://anime.nicovideo.jp/* // @match https://www.google.com/search?* // @match https://www.google.co.jp/search?* // @exclude *://ads*.nicovideo.jp/* // @exclude *://www.upload.nicovideo.jp/* // @exclude *://www.nicovideo.jp/watch/*?edit=* // @exclude *://ch.nicovideo.jp/tool/* // @exclude *://flapi.nicovideo.jp/* // @exclude *://dic.nicovideo.jp/p/* // @exclude *://ext.nicovideo.jp/thumb/* // @exclude *://ext.nicovideo.jp/thumb_channel/* // @version 0.3.16 // @grant none // @author segabito macmoto // @license public domain // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js // @downloadURL none // ==/UserScript== // 保留 // https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.23/webcomponents.js (function() { const PRODUCT = 'MylistPocket'; const monkey = function(PRODUCT) { const console = window.console; //const $ = window.jQuery; console.log(`exec ${PRODUCT}..`); const TOKEN = 'r:' + (Math.random()); const CONSTANT = { BASE_Z_INDEX: 100000 }; const MylistPocket = {debug: {}}; window.MylistPocket = MylistPocket; const protocol = location.hostname === 'www.nicovideo.jp' ? location.protocol : 'http:'; const __css__ = (` a[href*='watch/'] { display: inline-block; } a[href*='watch/'] > g-img { position: inherit; } .mylistPocketHoverMenu { display: none; opacity: 0.8; position: absolute; z-index: ${CONSTANT.BASE_Z_INDEX + 100000}; font-size: 8pt; padding: 0; line-height: 26px; font-weight: bold; text-align: center; transition: box-shadow 0.2s ease, opacity 0.4s ease, padding 0.2s ease; user-select: none; } .mylistPocketHoverMenu.is-busy { opacity: 0 !important; pointer-events: none; } .mylistPocketHoverMenu.is-otherDomain .wwwOnly { display: none; } .mylistPocketHoverMenu.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly { display: none; } .mylistPocketHoverMenu .zenzaMenu { display: none; } .mylistPocketHoverMenu.is-zenzaReady .zenzaMenu { display: inline-block; } .mylistPocketButton { /*font-family: Menlo;*/ display: block; font-weight: bolder; cursor: pointer; width: 32px; height: 26px; cursor: pointer; box-shadow: 1px 1px 1px #000; transition: 0.1s box-shadow ease, 0.1s transform ease; font-size: 16px; line-height: 24px; -webkit-user-select: none; -moz-use-select: none; user-select: none; outline: none; } .mylistPocketButton:hover { transform: scale(1.2); box-shadow: 4px 4px 5px #000; } .mylistPocketButton:active { transform: scale(1.0); box-shadow: none; transition: none; } .is-deflistUpdating .mylistPocketButton.deflist-add::after, .is-deflistSuccess .mylistPocketButton.deflist-add::after, .is-deflistFail .mylistPocketButton.deflist-add::after, .mylistPocketButton:hover::after, #mylistPocket-poupup [tooltip] { content: attr(tooltip); position: absolute; /*top: 0px; left: 50%;*/ top: 50%; right: -8px; padding: 2px 4px; white-space: nowrap; font-size: 12px; color: #fff; background: #333; transform: translate3d(-50%, -120%, 0); transform: translate3d(100%, -50%, 0); pointer-events: none; } .is-deflistUpdating .mylistPocketButton.deflist-add { cursor: wait; opacity: 0.9; transform: scale(1.0); box-shadow: none; transition: none; background: #888; border-style: inset; } .is-deflistSuccess .mylistPocketButton.deflist-add, .is-deflistFail .mylistPocketButton.deflist-add { transform: scale(1.0); box-shadow: none; transition: none; } .is-deflistSuccess .mylistPocketButton.deflist-add::after { content: attr(data-result); background: #393; } .is-deflistFail .mylistPocketButton.deflist-add::after { content: attr(data-result); background: #933; } .is-deflistUpdating .mylistPocketButton.deflist-add::after { content: '更新中'; background: #333; } .mylistPocketButton + .mylistPocketButton { margin-top: 4px; } .mylistPocketHoverMenu:hover { font-weibht: bolder; opacity: 1; } .mylistPocketHoverMenu:active { } .mylistPocketHoverMenu.is-show { display: block; } #mylistPocket-popup { display: none; perspective: 800px; } #mylistPocket-popup.is-firefox { /*perspective: none !important;*/ position: fixed; z-index: 200000; transform: translate3d(-50%, -50%, 0); opacity: 0; transition: 0.3s opacity ease; top: -9999px; left: -9999px; } #mylistPocket-popup.show { display: block; } #mylistPocket-popup.is-firefox.show { top: 50%; left: 50%; opacity: 1; } #mylistPocket-popup .owner-icon { width: 64px; height: 64px; transform-origin: center; transform-origin: center; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .owner-icon:hover { } #mylistPocket-popup .description a { color: #ffff00 !important; text-decoration: none !important; font-weight: normal !important; display: inline-block; } #mylistPocket-popup .description a.watch { display: block; } #mylistPocket-popup .description a:visited { color: #ffff99 !important; } #mylistPocket-popup .description button { /*font-family: Menlo;*/ font-size: 16px; font-weight: bolder; margin: 4px 8px; padding: 4px 8px; cursor: pointer; border-radius: 0; background: #333; color: #ccc; border: solid 2px #ccc; outline: none; } #mylistPocket-popup .description button:hover { transform: translate(-2px,-2px); box-shadow: 2px 2px 2px #000; background: #666; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .description button:active { transform: none; box-shadow: none; transition: none; } #mylistPocket-popup .description button:active::hover { opacity: 0; } #mylistPocket-popup .watch { display: block; position: relative; line-height: 60px; box-sizing: border-box; padding: 4px 16px;; min-height: 60px; width: 280px; margin: 8px 10px; background: #444; border-radius: 4px; } #mylistPocket-popup .watch:hover { background: #446; } #mylistPocket-popup .videoThumbnail { position: absolute; right: 16px; height: 60px; transform-origin: center; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .videoThumbnail:hover { transform: scale(2); box-shadow: 0 0 8px #888; transition: 0.2s transform ease 0.5s, 0.2s box-shadow ease 0.5s ; } .zenzaPlayerContainer.is-error #mylistPocket-popup, .zenzaPlayerContainer.is-loading #mylistPocket-popup, .zenzaPlayerContainer.error #mylistPocket-popup, .zenzaPlayerContainer.loading #mylistPocket-popup { opacity: 0; pointer-events: none; } .mylistPocketHoverMenu.is-guest .is-need-login { display: none !important; } .xDomainLoaderFrame { position: fixed; left: -100%; top: -100%; width: 64px; height: 64px; opacity: 0; border: 0; } `).trim(); const nicoadHideCss = ` .nicoadVideoItem { display: none; } `.trim(); const __tpl__ = (`
【実況】どんぐりころころの大冒険 Part1(最終回) 1970/01/01 00:00 12,345 6,789 2,525 1:23 1234 ほげほげ
`).trim(); const __ng_css__ = ` /* .item_cell 将棋盤ランキング .item 従来のランキングと検索 */ .item_cell.is-ng-wait .item, .item.is-ng-wait { outline: 1px dotted rgba(192, 192, 192, 0.8); } .item_cell.is-ng-queue .item, .item.is-ng-queue { outline: 2px dotted rgba(192, 192, 192, 0.8); } .item_cell.is-ng-current .item, .item.is-ng-current { outline: 3px dotted rgba(128, 225, 128, 0.8); } .item_cell.is-ng-resolved .item, .item.is-ng-resolved { outline: 0px solid green; } .item_cell.is-fav-favorited .item, .item.is-fav-favorited { outline: 3px dotted orange; outline-offset: 3px; } .item.videoRanking.is-fav-favorited { outline-offset: -3px; } .item_cell.is-ng-rejected { opacity: 0; pointer-events: none; } .item.is-ng-rejected { display: none; opacity: 0; pointer-events: none; } .NicorepoTimelineItem.is-ng-rejected { display: none; opacity: 0; pointer-events: none; } body.is-ng-disable .is-ng-rejected { outline: none; display: block !important; pointer-events: auto; opacity: 0.5; } /*.is-ng-failed:not(.COMMUNITY) { outline: black dotted 1px; opacity: 0.5; }*/ /* チャンネル検索 */ #search .item.is-ng-rejected { display: none; } /* 新検索β */ #row-results .is-ng-wait { outline: 1px dotted rgba(192, 192, 192, 0.8); } #row-results .is-ng-queue { outline: 2px dotted rgba(192, 192, 192, 0.8); } #row-results .is-ng-rejected { display: none; } #row-results .is-fav-favorited { outline: 4px dotted pink; } `; // TODO: ライブラリ化 const util = MylistPocket.util = (() => { const util = {}; const addedStyle = {}; util.addStyle = function(styles, id) { var elm = document.createElement('style'); elm.type = 'text/css'; if (id) { elm.id = id; } if (addedStyle[styles]) { return; } addedStyle[styles] = 1; var text = styles.toString(); text = document.createTextNode(text); elm.appendChild(text); var head = document.getElementsByTagName('head'); head = head[0]; head.appendChild(elm); return elm; }; util.mixin = function(self, o) { _.each(Object.keys(o), f => { if (!_.isFunction(o[f])) { return; } if (_.isFunction(self[f])) { return; } self[f] = o[f].bind(o); }); }; util.createWebWorker = function(func) { const src = func.toString().replace(/^function.*?\{/, '').replace(/}$/, ''); const blob = new Blob([src], {type: 'text\/javascript'}); const url = URL.createObjectURL(blob); return new Worker(url); }; util.attachShadowDom = function({host, tpl, mode = 'open'}) { const root = host.attachShadow ? host.attachShadow({mode}) : host.createShadowRoot(); const node = document.importNode(tpl.content, true); root.appendChild(node); return root; }; util.getWatchId = function(url) { /\/?watch\/([a-z0-9]+)/.test(url || location.pathname); return RegExp.$1; }; util.isLogin = function() { return document.getElementsByClassName('siteHeaderLogin').length < 1; }; util.escapeHtml = function(text) { var map = { '&': '&', '\x27': ''', '"': '"', '<': '<', '>': '>' }; return text.replace(/[&"'<>]/g, char => { return map[char]; }); }; util.unescapeHtml = function(text) { var map = { '&' : '&' , ''' : '\x27', '"' : '"', '<' : '<', '>' : '>' }; return text.replace(/(&|'|"|<|>)/g, char => { return map[char]; }); }; util.escapeRegs = text => { let match = /[\\^$.*+?()[\]{}|]/g; // return text.replace(/[\\\*\+\.\?\{\}\(\)\[\]\^\$\-\|\/]/g, char => { return text.replace(match, '\\$&'); }; util.hasLargeThumbnail = function(videoId) { // return true; // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳 // ※この数字以降でもごく稀に例外はある。 var threthold = 16371888; var cid = videoId.substr(0, 2); if (cid !== 'sm') { return false; } var fid = videoId.substr(2) * 1; if (fid < threthold) { return false; } return true; }; util.httpLink = function(html) { let links = {}, keyCount = 0; const getTmpKey = function() { return ` `; }; html = html.replace(/@([a-zA-Z0-9_]+)/g, (g, id) => { const tmpKey = getTmpKey(); links[tmpKey] = ` @${id} `; return tmpKey; }); html = html.replace(/(im)(\d+)/g, ` $1$2 `); html = html.replace(/(co)(\d+)/g, ` $1$2 `); html = html.replace(/(watch|mylist|user)\/(\d+)/g, ` $1/$2 `); html = html.replace(/(sm|nm|so)(\d+)/g, ` $1$2 `); let linkmatch = //, n; html = html.split('
').join('
'); while ((n = linkmatch.exec(html)) !== null) { let tmpKey = getTmpKey(); links[tmpKey] = n; html = html.replace(n, tmpKey); } html = html.replace(/\((https?:\/\/[\x21-\x3b\x3d-\x7e]+)\)/gi, '( $1 )'); html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)http/gi, '$1 http'); html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)/gi, '$1'); Object.keys(links).forEach(tmpKey => { html = html.replace(tmpKey, links[tmpKey]); }); html = html.split('
').join('
'); return html; }; util.getSleepPromise = function(sleepTime, label = 'sleep') { return function(result) { return new Promise(resolve => { //console.time('sleep promise...' + label); window.setTimeout(() => { //console.timeEnd('sleep promise...' + label); return resolve(result); }, sleepTime); }); }; }; util.getPageLanguage = function() { try { var h = document.getElementsByClassName('html')[0]; return h.lang || 'ja-JP'; } catch(e) { return 'ja-JP'; } }; const videoIdReg = /^[a-z]{2}\d+$/; util.getThumbnailUrlByVideoId = function(videoId) { if (!videoIdReg.test(videoId)) { return null; } const fileId = parseInt(videoId.substr(2), 10); const num = (fileId % 4) + 1; const large = util.hasLargeThumbnail(videoId) ? '.L' : ''; return '//tn.smilevideo.jp/smile?i=' + fileId + large; }; util.isFirefox = function() { return navigator.userAgent.toLowerCase().indexOf('firefox') >= 0; }; return util; })(); class Emitter { constructor() { } on(name, callback) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events[name]) { this._events[name] = []; } this._events[name].push(callback); } clear(name) { if (!this._events) { this._events = {}; } if (name) { this._events[name] = []; } else { this._events = {}; } } emit(name) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events.hasOwnProperty(name)) { return; } const e = this._events[name]; const arg = Array.prototype.slice.call(arguments, 1); for (let i =0, len = e.length; i < len; i++) { e[i].apply(null, arg); } } emitAsync(...args) { window.setTimeout(() => { this.emit(...args); }, 0); } } MylistPocket.emitter = util.emitter = new Emitter(); const ZenzaDetector = (function() { let isReady = false; let Zenza = null; const emitter = new Emitter(); const initialize = function() { const onZenzaReady = () => { isReady = true; Zenza = window.ZenzaWatch; Zenza.emitter.on('hideHover', () => { util.emitter.emit('hideHover'); }); Zenza.emitter.on('csrfToken', (token) => { util.emitter.emit('csrfToken', token); }); let popup = document.getElementById('mylistPocket-popup'); let defaultContainer = document.getElementById('mylistPocketDomContainer'); let zenzaContainer; Zenza.emitter.on('fullScreenStatusChange', isFull => { if (isFull) { if (!zenzaContainer) { zenzaContainer = document.querySelector('.zenzaPlayerContainer'); } zenzaContainer.appendChild(popup); } else { defaultContainer.appendChild(popup); } }); emitter.emit('ready', Zenza); }; if (window.ZenzaWatch && window.ZenzaWatch.ready) { window.console.log('ZenzaWatch is Ready'); onZenzaReady(); } else { document.body.addEventListener('ZenzaWatchInitialize', function() { window.console.log('ZenzaWatchInitialize MylistPocket'); onZenzaReady(); }); } }; const detect = function() { return new Promise(res => { if (isReady) { return res(Zenza); } emitter.on('ready', () => { res(Zenza); }); }); }; return { initialize: initialize, detect: detect }; })(); const StorageWriter = (function() { const emitter = new Emitter(); // マイページのJSON.stringifyがPrototype.jsのせいでぶっこわれているので // 汚染されていないWebWorkerを使って書き込む const func = function(self) { self.onmessage = function(e) { const key = e.data.key; const value = e.data.value; const storage = e.data.storage; self.postMessage({key, value: JSON.stringify(value), storage}); }; }; const worker = util.createWebWorker(func); worker.addEventListener('message', (e) => { const key = e.data.key; const value = e.data.value; const storage = e.data.storage === 'session' ? sessionStorage : localStorage; storage[key] = value; emitter.emit('write', {key, value, storage: e.data.storage}); }); return { write: function({key, value, storage = 'local'}) { worker.postMessage({ key, value, storage }); }, on: function(...args) { emitter.on(...args); } }; })(); MylistPocket.debug.writer = StorageWriter; const config = (function() { const prefix = PRODUCT + '_config_'; const emitter = new Emitter(); const defaultConfig = { debug: false, 'videoInfo.openNewWindow': false, 'mylist.enableAutoComment': true, // マイリストコメントに投稿者を入れる 'nicoad.hide': false, 'ng.enable': false, 'ng.owner': '', 'ng.word': '', 'ng.tag': '', 'ng.syncZenza': false, 'fav.owner': '', 'fav.word': '', 'fav.tag': '' }; const config = {}; let noEmit = false; emitter.refresh = (emitChange = false) => { //console.log('keys', Object.keys(localStorage)); Object.keys(defaultConfig).forEach(key => { var storageKey = prefix + key; //console.log('conf', storageKey, localStorage.hasOwnProperty(storageKey), localStorage[storageKey]); if (localStorage.hasOwnProperty(storageKey) || localStorage[storageKey] !== undefined) { try { let lastValue = config[key]; let newValue = JSON.parse(localStorage.getItem(storageKey)); if (lastValue !== newValue) { config[key] = newValue; if (emitChange) { emitter.emit('key', newValue); emitter.emit('@update', {key, value: newValue}); } } } catch (e) { window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e); config[key] = defaultConfig[key]; } } else { config[key] = defaultConfig[key]; } }); }; emitter.refresh(); //Chrome Canary v60でlocalStorageにデータが有ってもhasOwnPropertyがfalseを返したりする //window.fuga = window.localStorage; //function dump() { // Object.keys(window.fuga).forEach(key => { // console.log('DUMP key="%s" hasOwn="%s" value="%s"', key, window.fuga.hasOwnProperty(key), window.fuga.getItem(key)); // }); //} //dump(); //window.setTimeout(dump, 3000); emitter.getValue = function(key, refresh) { if (refresh) { emitter.refreshValue(key); } return config[key]; }; emitter.setValue = function(key, value) { if (config[key] !== value && arguments.length >= 2) { var storageKey = prefix + key; StorageWriter.write({key: storageKey, value}); config[key] = value; emitter.emit(key, value); emitter.emit('@update', {key, value}); //console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value); } }; emitter.clearConfig = function() { noEmit = true; Object.keys(defaultConfig).forEach(key => { if (['message', 'debug'].includes(key)) { return; } var storageKey = prefix + key; try { if (localStorage.hasOwnProperty(storageKey) || localStorage[storageKey] !== undefined) { localStorage.removeItem(storageKey); } config[key] = defaultConfig[key]; } catch (e) {} }); noEmit = false; }; emitter.getKeys = function() { return Object.keys(defaultConfig); }; emitter.namespace = function(name) { return { getValue: (key) => { return emitter.getValue(name + '.' + key); }, setValue: (key, value) => { emitter.setValue(name + '.' + key, value); }, refresh: () => { emitter.refresh(); }, on: (key, func) => { if (key === '@update') { emitter.on('@update', ({key, value}) => { const pre = name + '.'; //console.log('@update', key, value, pre); if (key.startsWith(pre)) { func({key: key.replace(pre, ''), value}); } }); } else { emitter.on(name + '.' + key, func); } } }; }; StorageWriter.on('write', ({key, value, storage}) => { if (storage === 'session') { return; } const _key = key.replace(prefix, ''); if (!config.hasOwnProperty(_key)) { return; } MylistPocket.broadcast.postMessage( {type: 'config-update', key, value, storage} ); }); return emitter; })(); MylistPocket.config = config; MylistPocket.broadcast = (function(config) { if (!window.BroadcastChannel) { return; } const broadcastChannel = new window.BroadcastChannel(PRODUCT); const onBroadcastMessage = (e) => { const data = e.data; //window.console.info(`${PRODUCT}: onBroadcastMessage`, data); switch (data.type) { case 'config-update': config.refresh(true); break; } }; broadcastChannel.addEventListener('message', onBroadcastMessage); return { postMessage: (...args) => { broadcastChannel.postMessage(...args); } }; })(config); const CacheStorage = (function() { var PREFIX = PRODUCT + '_cache_'; class CacheStorage { constructor(storage, gc = false) { this._storage = storage; this._memory = {}; if (gc) { this.gc(); } Object.keys(storage).forEach((key) => { if (key.indexOf(PREFIX) === 0) { this._memory[key] = storage[key]; } }); this.gc = _.debounce(this.gc.bind(this), 100); } gc(now = -1) { const storage = this._storage; now = now >= 0 ? now : Date.now(); Object.keys(storage).forEach((key, index) => { if (key.indexOf(PREFIX) === 0) { let item; try { item = JSON.parse(this._storage[key]); } catch(e) { storage.removeItem(key); } //console.info( // `${index}, key: ${key}, expiredAt: ${new Date(item.expiredAt).toLocaleString()}, now: ${new Date(now).toLocaleString()}`); if (item.expiredAt === '' || item.expiredAt > now) { //console.info('not expired: ', key); return; } //console.info('cache expired: ', key, item.expiredAt); storage.removeItem(key); } }); } setItem(key, data, expireTime) { key = PREFIX + key; const expiredAt = typeof expireTime === 'number' ? (Date.now() + expireTime) : ''; const cacheData = { data: data, type: typeof data, expiredAt: expiredAt }; this._memory[key] = cacheData; try { StorageWriter.write({ key, value: cacheData, storage: this._storage === sessionStorage ? 'session' : 'local' }); //this._storage[key] = JSON.stringify(cacheData); this.gc(); } catch (e) { if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { this.gc(0); } } } getItem(key) { key = PREFIX + key; if (!(this._storage.hasOwnProperty(key) || this._storage[key] !== undefined)) { return null; } let item = null; try { item = JSON.parse(this._storage[key]); } catch(e) { delete this._memory[key]; this._storage.removeItem(key); return null; } if (item.expiredAt === '' || item.expiredAt > Date.now()) { return item.data; } return null; } removeItem(key) { if (this._memory.hasOwnProperty(key)) { delete this._memory[key]; } key = PREFIX + key; if (this._storage.hasOwnProperty(key) || this._storage[key] !== undefined) { this._storage.removeItem(key); } } clear() { const storage = this._storage; this._memory = {}; Object.keys(storage).forEach((v) => { if (v.indexOf(PREFIX) === 0) { storage.removeItem(v); } }); } } return CacheStorage; })(); MylistPocket.debug.sessionCache = new CacheStorage(sessionStorage, true); MylistPocket.debug.localCache = new CacheStorage(localStorage, true); const WindowMessageEmitter = (function() { const emitter = new Emitter(); const knownSource = []; const onMessage = (event) => { if (_.indexOf(knownSource, event.source) < 0 //&& //event.origin !== location.protocol + '//ext.nicovideo.jp' ) { return; } try { var data = JSON.parse(event.data); if (data.id !== PRODUCT) { return; } emitter.emit('onMessage', data.body, data.type); } catch (e) { console.log( '%cMylistPocket.Error: window.onMessage - ', 'color: red; background: yellow', e, event ); console.log('%corigin: ', 'background: yellow;', event.origin); console.log('%cdata: ', 'background: yellow;', event.data); console.trace(); } }; emitter.addKnownSource = (win) => { knownSource.push(win); }; window.addEventListener('message', onMessage); return emitter; })(); const CrossDomainGate = (function() { class CrossDomainGate extends Emitter { constructor(params) { super(); this._baseUrl = params.baseUrl; this._origin = params.origin || location.href; this._type = params.type; this._messager = params.messager || WindowMessageEmitter; this._loaderFrame = null; this._sessions = {}; this._initializeStatus = ''; } _initializeFrame() { switch (this._initializeStatus) { case 'done': return new Promise((resolve) => { window.setTimeout(() => { resolve(); }, 0); }); case 'initializing': return new Promise((resolve, reject) => { this.on('initialize', (e) => { if (e.status === 'ok') { resolve(); } else { reject(e); } }); }); case '': this._initializeStatus = 'initializing'; var initialPromise = new Promise((resolve, reject) => { this._sessions.initial = { promise: initialPromise, resolve: resolve, reject: reject }; setTimeout(() => { if (this._initializeStatus !== 'done') { var rej = { status: 'fail', message: 'CrossDomainGate初期化タイムアウト (' + this._type + ')' }; reject(rej); this.emit('initialize', rej); } }, 60 * 1000); this._initializeCrossDomainGate(); }); return initialPromise; } } _initializeCrossDomainGate() { this._initializeCrossDomainGate = _.noop; this._messager.on('onMessage', this._onMessage.bind(this)); console.log('%c initialize ' + this._type, 'background: lightgreen;'); const loaderFrame = document.createElement('iframe'); loaderFrame.name = this._type + 'Loader'; loaderFrame.className = 'xDomainLoaderFrame ' + this._type; document.body.appendChild(loaderFrame); this._loaderFrame = loaderFrame; this._loaderWindow = loaderFrame.contentWindow; this._messager.addKnownSource(this._loaderWindow); this._loaderWindow.location.href = this._baseUrl + '#' + TOKEN; } _onMessage(data, type) { if (type !== this._type) { return; } const info = data.message; const token = info.token; const sessionId = info.sessionId; const status = info.status; const command = info.command || 'loadUrl'; let session = this._sessions[sessionId]; if (status === 'initialized') { this._initializeStatus = 'done'; this._sessions.initial.resolve(); this.emitAsync('initialize', {status: 'ok'}); return; } if (token !== TOKEN) { window.console.log('invalid token:', token, TOKEN); return; } switch (command) { case 'dumpConfig': this._onDumpConfig(info.body); break; default: if (!session) { return; } if (status === 'ok') { session.resolve(info.body); } else { session.reject({ message: status }); } session = null; delete this._sessions[sessionId]; break; } } load(url, options) { return this._postMessage({ command: 'loadUrl', url: url, options: options }, true); } _postMessage(message, needPromise) { return new Promise((resolve, reject) => { message.sessionId = this._type + '_' + Math.random(); message.token = TOKEN; if (needPromise) { this._sessions[message.sessionId] = { resolve: resolve, reject: reject }; } return this._initializeFrame().then(() => { try { this._loaderWindow.postMessage( JSON.stringify(message), this._origin ); } catch (e) { console.log('%cException!', 'background: red;', e); } }); }); } } return CrossDomainGate; })(); const CsrfTokenLoader = (() => { const cacheStorage = new CacheStorage( location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage); const TIMEOUT = 10 * 1000; const CACHE_EXPIRE_TIME = 60 * 30 * 1000; class CsrfTokenLoader { static load() { return new Promise((resolve, reject) => { const cache = cacheStorage.getItem('csrfToken'); if (cacheStorage.getItem('csrfToken')) { return resolve(cache); } let timeoutTimer = window.setTimeout(() => { reject('timeout'); }, TIMEOUT); return CsrfTokenLoader._getToken().then((token) => { window.clearTimeout(timeoutTimer); CsrfTokenLoader.saveToCache(token); resolve(token); }); }); } static saveToCache(token) { cacheStorage.setItem('csrfToken', token, CACHE_EXPIRE_TIME); } static _getToken() { const url = protocol + '//www.nicovideo.jp/mylist_add/video/sm9'; const tokenReg = /NicoAPI\.token *= *["']([a-z0-9\-]+)["'];/; return fetch(url, { credentials: 'include' }).then((res) => { return res.text(); }).then((result) => { if (tokenReg.test(result)) { let token = RegExp.$1; return Promise.resolve(token); } else { return Promise.reject('token parse error'); } }); } } util.emitter.on('csrfToken', (token) => { CsrfTokenLoader.saveToCache(token); }); return CsrfTokenLoader; })(); MylistPocket.debug.CsrfTokenLoader = CsrfTokenLoader; const ThumbInfoLoader = (() => { const BASE_URL = 'https://ext.nicovideo.jp/'; const MESSAGE_ORIGIN = 'https://ext.nicovideo.jp/'; const CACHE_EXPIRE_TIME = 60 * 60 * 1000; //const CACHE_EXPIRE_TIME = 60 * 1000; let gate = null; let cacheStorage = new CacheStorage(sessionStorage, true); class ThumbInfoLoader { constructor() { this._emitter = new Emitter(); gate = new CrossDomainGate({ baseUrl: BASE_URL, origin: MESSAGE_ORIGIN, type: 'thumbInfo' + PRODUCT, messager: WindowMessageEmitter }); } _onMessage(data, type) { if (type !== 'videoInfoLoader') { return; } const info = data.message; this.emit('load', info, 'THUMB_WATCH'); } _parseXml(xmlText) { const parser = new DOMParser(); const xml = parser.parseFromString(xmlText, 'text/xml'); const val = (name) => { var elms = xml.getElementsByTagName(name); if (elms.length < 1) { return ''; } return elms[0].innerHTML; }; const resp = xml.getElementsByTagName('nicovideo_thumb_response'); if (resp.length < 1 || resp[0].getAttribute('status') !== 'ok') { return { status: 'fail', code: val('code'), message: val('description') }; } const duration = (() => { const tmp = val('length').split(':'); return parseInt(tmp[0], 10) * 60 + parseInt(tmp[1], 10); })(); const watchId = val('watch_url').split('/').reverse()[0]; const postedAt = (new Date(val('first_retrieve'))).toLocaleString(); const tags = (() => { const result = [], t = xml.getElementsByTagName('tag'); _.each(t, (tag) => { result.push({ text: tag.innerHTML, category: tag.hasAttribute('category'), lock: tag.hasAttribute('lock') }); }); return result; })(); const result = { status: 'ok', _format: 'thumbInfo', v: watchId, id: val('video_id') || '', title: val('title'), description: val('description'), thumbnail: val('thumbnail_url'), movieType: val('movie_type'), lastResBody: val('last_res_body'), duration: duration, postedAt: postedAt, mylistCount: parseInt(val('mylist_counter'), 10), viewCount: parseInt(val('view_counter'), 10), commentCount: parseInt(val('comment_num'), 10), tagList: tags }; const userId = val('user_id'); if (userId !== '') { result.owner = { type: 'user', id: userId, name: val('user_nickname') || '(非公開ユーザー)', url: userId ? (protocol + '//www.nicovideo.jp/user/' + userId) : '#', icon: val('user_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg' }; } const channelId = val('ch_id'); if (channelId !== '') { result.owner = { type: 'channel', id: channelId, name: val('ch_name') || '(非公開ユーザー)', url: protocol + '//ch.nicovideo.jp/ch' + channelId, icon: val('ch_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg' }; } return result; } loadXml(watchId) { return this.load(watchId, 'xml'); } load(watchId, format) { return new Promise((resolve, reject) => { const cache = cacheStorage.getItem('thumbInfo_' + watchId); const onLoad = (xml) => { const result = this._parseXml(xml); result.fromCache = !!cache; if (result.status === 'ok') { if (!cache) { cacheStorage.setItem('thumbInfo_' + watchId, xml, CACHE_EXPIRE_TIME); } resolve({data: format === 'xml' ? xml : result, watchId}); } else { reject({data: format === 'xml' ? xml : result, watchId}); } }; if (cache) { //console.log('cache exist: ', watchId); onLoad(cache); //window.setTimeout(() => { onLoad(cache); }, 0); return; } gate.load(BASE_URL + 'api/getthumbinfo/' + watchId).then(onLoad); }); } } const loader = new ThumbInfoLoader(); return { load: (watchId) => { return loader.load(watchId); }, loadXml: (watchId) => { return loader.loadXml(watchId); }, loadOwnerInfo: (watchId) => { return loader.load(watchId).then((info) => { const owner = info.data.owner; if (!owner) { return {}; } const lang = util.getPageLanguage(); const prefix = owner.type === 'user' ? '投稿者: ' : '提供: '; const suffix = (owner.type === 'user' && lang === 'ja-JP') ? ' さん' : ''; owner.localeName = `${prefix}${owner.name}${suffix}`; return owner; }); } }; })(); MylistPocket.debug.ThumbInfoLoader = ThumbInfoLoader; const DeflistApiLoader = ((CsrfTokenLoader) => { const cacheStorage = new CacheStorage( location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage); const TIMEOUT = 30000; const CACHE_EXPIRE_TIME = 60 * 3 * 1000; let isZenzaReady = false; class DeflistApiLoader { static getItems() { const url = '//www.nicovideo.jp/api/deflist/list'; const cacheKey = 'deflistItems'; return new Promise(function(resolve, reject) { const cache = cacheStorage.getItem(cacheKey); if (cache) { window.setTimeout(() => { resolve({items: cache.mylistitem, status: cache.status, from: 'cache'}); }, 0); return; } let timeoutTimer = window.setTimeout(() => { timeoutTimer = null; reject({status: 'fail', description: 'timeout'}); }, TIMEOUT); fetch(url, { credentials: 'include' }).then((res) => { return res.json(); }).then((json) => { if (json.status !== 'ok') { return reject(json); } if (timeoutTimer) { window.clearTimeout(timeoutTimer); } else { return; } cacheStorage.setItem(cacheKey, json, CACHE_EXPIRE_TIME); resolve({items: json.mylistitem, status: json.status, from: 'fetch'}); }); }); } static findItemByWatchId(watchId) { return DeflistApiLoader.getItems().then(({items}) => { for (var i = 0, len = items.length; i < len; i++) { var item = items[i], wid = item.id || item.item_data.watch_id; if (wid === watchId) { return Promise.resolve(item); } } return Promise.reject(); }); } static _removeItem({watchId, token}) { const cacheKey = 'deflistItems'; DeflistApiLoader.findItemByWatchId(watchId).then((item) => { const url = '//www.nicovideo.jp/api/deflist/delete'; const body = 'id_list[0][]=' + item.item_id + '&token=' + token; const req = { credentials: 'include', method: 'post', body, headers: {'Content-Type': 'application/x-www-form-urlencoded'} }; return fetch(url, req) .then(res => { return res.json(); }) .then((result) => { if (result.status !== 'ok') { return Promise.reject({ status: 'fail', result: result, code: result.error.code, message: result.error.description }); } cacheStorage.removeItem(cacheKey); util.emitter.emitAsync('deflistRemove', watchId); return Promise.resolve({ status: 'ok', result: result, message: 'とりあえずマイリストから削除' }); }, (err) => { return Promise.reject({ result: err, message: 'とりあえずマイリストから削除失敗(2)' }); }); }, (err) => { return Promise.reject({ status: 'fail', result: err, message: '動画が見つかりません' }); }); } static removeItem(watchId) { return CsrfTokenLoader.load().then((token) => { return DeflistApiLoader._removeItem({watchId, token}); }); } static __addItem({watchId, description, token, isRetry = false}) { const cacheKey = 'deflistItems'; const url = '//www.nicovideo.jp/api/deflist/add'; let body = 'item_id=' + watchId + '&token=' + token; if (description) { body += '&description='+ encodeURIComponent(description); } const req = { method: 'post', credentials: 'include', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }; return new Promise((resolve, reject) => { fetch(url, req) .then((res) => { return res.json(); }) .then((result) => { if (result.status && result.status === 'ok') { cacheStorage.removeItem(cacheKey); //ZenzaWatch.emitter.emitAsync('deflistAdd', watchId, description); return resolve({ status: 'ok', result: result, message: 'とりあえずマイリスト登録' }); } if (!result.status || !result.error) { return reject({ status: 'fail', result: result, message: 'とりあえずマイリスト登録失敗(100)' }); } if (result.error.code !== 'EXIST' || isRetry) { return reject({ status: 'fail', result: result, code: result.error.code, message: result.error.description }); } /** * すでに登録されている場合は、いったん削除して再度追加(先頭に移動) */ return DeflistApiLoader.removeItem(watchId) .then(util.getSleepPromise(1500, 'deflist remove')) .then(() => { return DeflistApiLoader._addItem(watchId, description, true) .then((result) => { resolve({ status: 'ok', result: result, message: 'とりあえずマイリストの先頭に移動' }); }); }, (err) => { reject({ status: 'fail', result: err.result, code: err.code, message: 'とりあえずマイリスト登録失敗(101)' }); }); }, (err) => { reject({ status: 'fail', result: err, message: 'とりあえずマイリスト登録失敗(200)' }); }); }); } static _addItem(watchId, description, isRetry = false) { return CsrfTokenLoader.load().then((token) => { return DeflistApiLoader.__addItem({watchId, description, isRetry, token}); }); } static addItem(watchId, description) { return DeflistApiLoader._addItem(watchId, description, false); } static addItemWithOwnerName(watchId) { return ThumbInfoLoader.loadOwnerInfo(watchId).then((owner) => { if (!owner.id) { return DeflistApiLoader.addItem(watchId); } const description = owner.localeName; return DeflistApiLoader.addItem(watchId, description); }, () => DeflistApiLoader.addItem(watchId)); } static clearCache() { cacheStorage.removeItem('deflistItems'); } } ZenzaDetector.detect().then((ZenzaWatch) => { isZenzaReady = true; ZenzaWatch.emitter.on('deflistRemove', () => DeflistApiLoader.clearCache()); }); //DeflistApiLoader.clearCache(); return DeflistApiLoader; })(CsrfTokenLoader); MylistPocket.debug.DeflistApiLoader = DeflistApiLoader; class HoverMenu extends Emitter { constructor() { super(); this._init(); } _init() { this._view = document.querySelector('.mylistPocketHoverMenu'); this._view.addEventListener('click', this._onClick.bind(this)); this._view.addEventListener('mousedown', this._onMousedown.bind(this)); this._view.addEventListener('contextmenu', this._onContextMenu.bind(this)); this._onHoverEnd = _.debounce(this._onHoverEnd.bind(this), 500); document.body.addEventListener( 'mouseover', this._onHover.bind(this), {passive: true}); document.body.addEventListener( 'mouseout', this._onMouseout.bind(this), {passive: true}); document.body.addEventListener( 'mouseover', this._onHoverEnd, {passive: true}); document.body.addEventListener( 'click', () => { this.hide(); }, {passive: true}); util.emitter.on('hideHover', () => this.hide()); this._x = this._y = 0; ZenzaDetector.detect().then((ZenzaWatch) => { this._isZenzaReady = true; this.addClass('is-zenzaReady'); ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => { this.hide(); }, 1000)); }); this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp'); this.toggleClass('is-guest', !util.isLogin()); this._deflistButton = this._view.querySelector('.mylistPocketButton.deflist-add'); MylistPocket.debug.hoverMenu = this._view; } toggleClass(className, v) { className.split(/ +/).forEach((c) => { this._view.classList.toggle(c, v); }); } addClass(className) { this.toggleClass(className, true); } removeClass(className) { this.toggleClass(className, false); } hide() { this.removeClass('is-show'); } show() { this.addClass('is-show'); } moveTo(x, y) { this._x = x; this._y = y; this._view.style.left = x + 'px'; this._view.style.top = y + 'px'; } _onClick(e) { e.preventDefault(); e.stopPropagation(); } _onContextMenu(e) { e.preventDefault(); e.stopPropagation(); } _onMousedown(e) { const watchId = this._watchId; const target = e.target.classList.contains('command') ? e.target : e.target.closest('.command'); const command = target.getAttribute('data-command'); e.preventDefault(); e.stopPropagation(); if (command === 'info') { this._videoInfo(watchId); this.hide(); } else if (command === 'playlist-queue') { this.emit('playlist-queue', watchId, this); } else { if (e.button !== 0 || e.shiftKey) { this._deflistRemove(watchId); } else { this._deflist(watchId); } } } _videoInfo(watchId) { this.emit('info', watchId || this._watchId, this); } _deflist(watchId) { this.emit('deflist-add', watchId || this._watchId, this); } _deflistRemove(watchId) { this.emit('deflist-remove', watchId || this._watchId, this); } _onHover(e) { const target = this._isTargetElement(e); if (!target) { return; } this._hoverElement = target; } _onHoverEnd(e) { const target = e.target.tagName === 'A' ? e.target : e.target.closest('a'); if (!target || this._hoverElement !== target) { return; } const href = target.getAttribute('data-href') || target.getAttribute('href'); const watchId = util.getWatchId(href); const offset = target.getBoundingClientRect(); //const bodyOffset = document.body.getBoundingClientRect(); const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0; const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0; const left = offset.left + scrollLeft; const top = offset.top + scrollTop; const host = target.hostname; if (host !== 'www.nicovideo.jp' && host !== 'nico.ms') { return; } if (target.classList.contains('noHoverMenu')) { return; } if (!watchId.match(/^[a-z0-9]+$/)) { return; } if (watchId.indexOf('lv') === 0) { return; } this._watchId = watchId; this.show(); this.moveTo( left + target.offsetWidth - this._view.offsetWidth / 2, top + target.offsetHeight / 2 - this._view.offsetHeight / 2 ); } _onMouseout(e) { const target = this._isTargetElement(e); if (!target) { return; } if (this._hoverElement === e.target) { this._hoverElement = null; } } _isTargetElement(e) { const target = e.target.tagName === 'A' ? e.target : e.target.closest('a'); if (!target) { return false; } const href = target.href || ''; if (!/(watch\/[a-z0-9]+|nico\.ms\/[a-z0-9]+)/.test(href)) { return false; } return target; } set isBusy(v) { this._isBusy = v; this.toggleClass('is-busy', v); } get isBusy() { return !!this._isBusy; } notifyBeginDeflistUpdate(/*watchId*/) { this.addClass('is-deflistUpdating'); } notifyEndDeflistUpdate(result) { this.addClass('is-deflistSuccess'); window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000); //window.console.info('ok result', result); this._deflistButton.setAttribute('data-result', result.message || '登録しました'); this.removeClass('is-deflistUpdating'); } notifyFailDeflistUpdate(result) { this.addClass('is-deflistFail'); window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000); //window.console.info('fail result', result); this._deflistButton.setAttribute('data-result', result.message || '登録失敗'); this.removeClass('is-deflistUpdating'); } } class VideoInfoView extends Emitter { constructor({host, tpl}) { super(); this._host = host; this._tpl = tpl; this._slot = {}; this._config = config.namespace('videoInfo'); this._mylistConfig = config.namespace('mylist'); const ngConfig = this._ngConfig = config.namespace('ng'); const favConfig = this._favConfig = config.namespace('fav'); this._nicoadConfig = config.namespace('nicoad'); const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig}); this._ngChecker = ngChecker; this._favChecker = favChecker; } _initialize() { if (this._isInitialized) { return; } const host = this._host; const tpl = this._tpl; this._shadowRoot = util.attachShadowDom({host, tpl}); Array.prototype.forEach.call(this._host.querySelectorAll('*'), (elm) => { //this._host.querySelectorAll('*').forEach((elm) => { const slot = elm.getAttribute('slot'); if (!slot) { return; } //const type = elm.getAttribute('data-type') || 'string'; this._slot[slot] = elm; }); this._rootDom = this._shadowRoot.querySelector('.root'); this._hostDom = this._host; this._rootDom.addEventListener('mousedown', e => { e.stopPropagation(); }); this._rootDom.querySelector('.setting-panel-main').addEventListener('click', e => { e.stopPropagation(); }); this._initSettingPanel(); const updateNgEnable = v => { this.toggleClass('is-ng-enable', v); }; updateNgEnable(this._ngConfig.getValue('enable')); this._ngConfig.on('enable', updateNgEnable); Array.prototype.forEach.call( this._rootDom.querySelectorAll('.scrollable'), elm => { elm.addEventListener('wheel', this._cancelMouseWheel); }); this._rootDom.addEventListener('click', this._onClick.bind(this)); this._boundOnBodyMouseDown = this._onBodyMouseDown.bind(this); MylistPocket.debug.view = this; util.emitter.on('hideHover', () => { this.hide(); }); const debUpdateFavNg = _.debounce(this._updateFavNg.bind(this), 100); this._ngConfig .on('@update', debUpdateFavNg); this._favConfig .on('@update', debUpdateFavNg); //this._mylistConfig.on('@update', debUpdateFavNg); ZenzaDetector.detect().then(() => { this._isZenzaReady = true; this.addClass('is-zenzaReady'); window.ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => { this.hide(); }, 1000)); }); this._videoInfoArea = this._rootDom.querySelector('.video-info'); this._deflistButton = this._rootDom.querySelector('.mylistPocketButton.deflist-add'); this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp'); this.toggleClass('is-firefox', util.isFirefox()); this._isInitialized = true; } _initSettingPanel() { const onSettingFormChange = this._onSettingFormChange.bind(this); const refresh = () => { //console.log('refresh setting-form'); Array.from(this._rootDom.querySelectorAll('.setting-form')).forEach(elm => { const name = elm.getAttribute('data-config-name'); if (!name) { return; } const namespace = elm.getAttribute('data-config-namespace') || ''; let config = this._config; if (namespace === 'ng') { config = this._ngConfig; } if (namespace === 'fav') { config = this._favConfig; } if (namespace === 'mylist') { config = this._mylistConfig; } if (namespace === 'nicoad') { config = this._nicoadConfig; } const tagName = (elm.tagName.toLowerCase()).toLowerCase(); if (tagName === 'input') { const type = (elm.type || '').toLowerCase(); switch (type) { case 'checkbox': elm.checked = !!config.getValue(name); break; default: elm.value = config.getValue(name); break; } } else if (tagName === 'select' || tagName === 'textarea') { elm.value = config.getValue(name); } elm.removeEventListener('change', onSettingFormChange); elm.addEventListener('change', onSettingFormChange); }); }; const onUpdate = _.debounce(refresh, 100); const syncZenza = _.debounce(() => { if (!this._ngConfig.getValue('syncZenza') || !this._isZenzaReady) { return; } window.ZenzaWatch.config.setValue( 'videoTagFilter', this._ngConfig.getValue('tag')); window.ZenzaWatch.config.setValue( 'videoOwnerFilter', this._ngConfig.getValue('owner')); }, 1000); refresh(); this._config .on('@update', onUpdate); this._favConfig.on('@update', onUpdate); this._ngConfig.on('@update', () => { onUpdate(); syncZenza(); }); } _onSettingFormChange(e) { const elm = e.target; const name = elm.getAttribute('data-config-name'); if (!name) { return; } const namespace = elm.getAttribute('data-config-namespace') || ''; let config = this._config; if (namespace === 'ng') { config = this._ngConfig; } if (namespace === 'fav') { config = this._favConfig; } if (namespace === 'nicoad') { config = this._nicoadConfig; } if (namespace === 'mylist') { config = this._mylistConfig; } const tagName = (elm.tagName.toLowerCase()).toLowerCase(); if (tagName === 'input') { const type = (elm.type || '').toLowerCase(); switch (type) { case 'checkbox': config.setValue(name, elm.checked); break; default: config.setValue(name, elm.value); break; } } else if (tagName === 'select' || tagName === 'textarea') { config.setValue(name, elm.value); } } toggleClass(className, v) { className.split(/ +/).forEach((c) => { this._rootDom.classList.toggle(c, v); this._hostDom.classList.toggle(c, v); }); } addClass(className) { this.toggleClass(className, true); } removeClass(className) { this.toggleClass(className, false); } bind(videoInfo) { this._videoInfo = videoInfo; //console.info('status?', videoInfo.status, videoInfo.status === 'ok'); if (videoInfo.status === 'ok') { this._bindSuccess(videoInfo); } else { this._bindFail(videoInfo); } window.setTimeout(() => { this.removeClass('is-loading'); }, 0); } _onClick(e) { const t = e.target; const elm = t.classList.contains('command') ? t : e.target.closest('.command'); if (!elm) { return; } // 簡易 throttle if (elm.classList.contains('is-active')) { return; } elm.classList.add('is-active'); window.setTimeout(() => { elm.classList.remove('is-active'); }, 500); e.preventDefault(); e.stopPropagation(); const command = elm.getAttribute('data-command'); const param = elm.getAttribute('data-param'); switch (command) { case 'toggle-setting': this.toggleSettingPanel(); break; case 'add-ng-tag': case 'add-fav-tag': case 'toggle-ng-tag': case 'toggle-fav-tag': { const tag = elm.getAttribute('data-tag') || ''; if (!tag) { break; } this.emit('command', command, { watchId: this._videoInfo.watchId, value: tag }, this); } break; case 'add-ng-owner': case 'add-fav-owner': case 'toggle-ng-owner': case 'toggle-fav-owner': { let owner = (this._videoInfo.isChannel ? 'ch' : '') + this._videoInfo.ownerId + '#' + this._videoInfo.ownerName; this.emit('command', command, { watchId: this._videoInfo.watchId, value: owner }, this); } break; case 'close': this.hide(); break; default: this.emit('command', command, param, this); } } _updateFavNg() { if (!this._isInitialized) { return; } if (!this._videoInfo || this._videoInfo.status !== 'ok') { return; } const videoInfo = this._videoInfo; const ownerInfo = this._rootDom.querySelector('.owner-info'); ownerInfo.classList.toggle('is-favorited', this._favChecker.isMatchOwner(videoInfo.owner)); ownerInfo.classList.toggle('is-ng', this._ngChecker .isMatchOwner(videoInfo.owner)); Array.prototype.forEach.call( this._rootDom.querySelectorAll('.tag-container'), (elm) => { const tag = elm.getAttribute('data-tag'); elm.classList.toggle('is-favorited', this._favChecker.isMatchTag(tag)); elm.classList.toggle('is-ng', this._ngChecker.isMatchTag(tag)); }); } _cancelMouseWheel(e) { const target = this; const delta = -e.deltaY; const scrollTop = target.scrollTop; const height = target.offsetHeight; const scrollHeight = target.scrollHeight; if (delta > 0) { // up if (scrollTop <= delta) { target.scrollTop = 0; e.preventDefault(); e.stopPropagation(); } } else if (scrollTop >= scrollHeight - height + delta) { // down target.scrollTop = scrollHeight - height; e.preventDefault(); e.stopPropagation(); } } toggleSettingPanel() { this.toggleClass('is-setting'); } _onBodyMouseDown() { document.body.removeEventListener('mousedown', this._boundOnBodyMouseDown); this.hide(); } reset() { this._initialize(); window.setTimeout(() => { this._videoInfoArea.scrollTop = 0; }, 0); this.removeClass('noclip'); this.addClass('is-loading'); } show() { this.addClass('show'); document.body.addEventListener('mousedown', this._boundOnBodyMouseDown); } hide() { this._videoInfoArea.scrollTop = 0; this.removeClass('show is-ok is-fail noclip is-setting'); } _bindSuccess(videoInfo) { const toCamel = p => { return p.replace(/-./g, s => { return s.charAt(1).toUpperCase(); }); }; Object.keys(this._slot).forEach((key) => { const camelKey = toCamel(key); const data = videoInfo[camelKey]; //console.log('keys', typeof data, key, camelKey, data); if (typeof data !== 'string' && typeof data !== 'object') { return; } const elm = this._slot[key]; const type = elm.getAttribute('data-type') || 'string'; switch (type) { case 'html': this._createDescription(elm, data); break; case 'int': let i = parseInt(data, 10); i = i.toLocaleString ? i.toLocaleString() : i; elm.textContent = i; break; case 'link': elm.href = data; break; case 'image': elm.src = data; break; case 'date': elm.textContent = data.toLocaleString(); break; default: elm.textContent = data; } }); const df = document.createDocumentFragment(); //Array.prototype.forEach.call(this._host.querySelectorAll('.tag'), t => { t.remove(); }); videoInfo.tags.forEach(tag => { df.appendChild((this._createTagSlot(tag, videoInfo))); }); const videoTags = this._rootDom.querySelector('.video-tags'); videoTags.innerHTML = ''; videoTags.appendChild(df); Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-watch-id'), elm => { elm.setAttribute('data-param', videoInfo.watchId); }); Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-video-id'), elm => { elm.setAttribute('data-param', videoInfo.videoId); }); const target = this._config.getValue('openNewWindow') ? '_blank' : '_self'; Array.prototype.forEach.call( this._host.querySelectorAll('.target-change'), elm => { elm.target = target; elm.rel = 'noopener'; }); this._updateFavNg(); this.toggleClass('is-channel', videoInfo.isChannel); this.addClass('is-ok'); this.removeClass('is-fail'); window.setTimeout(() => { this.addClass('noclip'); }, 800); } _createDescription(elm, data) { elm.innerHTML = util.httpLink(data); const watchReg = /watch\/([a-z0-9]+)/; const isZenzaReady = this._isZenzaReady; //if (util.isFirefox()) { return; } Array.from(elm.querySelectorAll('.videoLink[href*=\'watch/\']')).forEach((link) => { const href = link.getAttribute('href'); if (!watchReg.test(href)) { return; } const watchId = RegExp.$1; if (isZenzaReady) { link.classList.add('noHoverMenu'); link.classList.add('command'); link.setAttribute('data-command', 'zenza-open'); link.setAttribute('data-param', watchId); } const btn = document.createElement('button'); btn.innerHTML = '?'; btn.className = 'command command-button noHoverMenu'; btn.setAttribute('slot', 'command-button'); btn.setAttribute('tooltip', '動画情報'); btn.setAttribute('data-command', 'info'); btn.setAttribute('data-param', watchId); link.appendChild(btn); const thumbnail = util.getThumbnailUrlByVideoId(watchId); if (thumbnail) { const img = document.createElement('img'); img.className = 'videoThumbnail'; img.src = thumbnail; link.classList.add('popupThumbnail'); link.appendChild(img); } link.classList.add('watch'); }); } _bindFail(videoInfo) { this._slot['error-description'].textContent = `動画情報の取得に失敗しました (${videoInfo.description})`; this.addClass('is-fail'); this.removeClass('is-ok'); } _createTagSlot(tag, {isChannel, owner}) { const text = util.escapeHtml(tag.text); const lock = tag.isLocked ? 'is-locked' : ''; const span = document.createElement('span'); const ownerId = owner ? owner.id : ''; const a = document.createElement('a'); const target = this._config.getValue('openNewWindow') ? '_blank' : '_self'; a.textContent = tag.text; a.className = `tag ${lock}`; a.target = target; a.rel = 'noopener'; a.href = `http://www.nicovideo.jp/tag/${encodeURIComponent(text)}`; span.appendChild(a); if (isChannel) { const ch = document.createElement('a'); const target = this._config.getValue('openNewWindow') ? '_blank' : '_self'; ch.textContent = '[ch]'; ch.className = `tag ${lock} channel-search`; ch.target = target; ch.rel = 'noopener'; ch.title = 'チャンネル検索'; //ch.href = `http://ch.nicovideo.jp/search/${encodeURIComponent(text)}?channel_id=ch${ownerId}&type=video&mode=t`; ch.href = `http://ch.nicovideo.jp/search/${encodeURIComponent(text)}?type=video&mode=t`; span.appendChild(ch); } const fav = document.createElement('button'); fav.className = 'add-fav-button command'; fav.setAttribute('data-command', 'toggle-fav-tag'); fav.setAttribute('data-tag', tag.text); fav.innerHTML = '★'; //'⃠'; // ✖ span.appendChild(fav); const bt = document.createElement('button'); bt.className = 'add-ng-button command'; bt.setAttribute('data-command', 'toggle-ng-tag'); bt.setAttribute('data-tag', tag.text); bt.innerHTML = '✖'; //'⃠'; // ✖ span.appendChild(bt); span.className = 'tag-container'; span.setAttribute('data-tag', tag.text); span.slot = 'tag'; return span; } notifyBeginDeflistUpdate(/*watchId*/) { this.addClass('is-deflistUpdating'); } notifyEndDeflistUpdate(result) { this.addClass('is-deflistSuccess'); window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000); //window.console.info('ok result', result); this._deflistButton.setAttribute('data-result', result.message || '登録しました'); this.removeClass('is-deflistUpdating'); } notifyFailDeflistUpdate(result) { this.addClass('is-deflistFail'); window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000); //window.console.info('fail result', result); this._deflistButton.setAttribute('data-result', result.message || '登録失敗'); this.removeClass('is-deflistUpdating'); } } class VideoInfo { static createByThumbInfo({xml, watchId}) { const dom = (new DOMParser()).parseFromString(xml, 'text/xml'); const status = dom.getElementsByTagName('nicovideo_thumb_response')[0].getAttribute('status'); //console.info('status', status); const t = function(name) { const tt = dom.getElementsByTagName(name); if (!tt || !tt[0]) { return ''; } return tt[0].textContent.trim(); }; const videoId = t('video_id'); let thumbnail = t('thumbnail_url'); if (util.hasLargeThumbnail(videoId)) { thumbnail += '.L'; } const isChannel = !!t('ch_id'); const tags = []; const rawData = { status, videoId: t('video_id'), watchId: isChannel ? t('video_id') : watchId, videoTitle: t('title'), videoThumbnail: thumbnail, uploadDate: t('first_retrieve'), duration: t('length'), viewCounter: t('view_counter'), mylistCounter: t('mylist_counter'), commentCounter: t('comment_num'), description: t('description'), lastResBody: t('last_res_body'), isChannel, ownerId: isChannel ? t('ch_id') : t('user_id'), ownerName: isChannel ? t('ch_name') : t('user_nickname'), ownerIcon: isChannel ? t('ch_icon_url') : t('user_icon_url'), tags }; dom.querySelectorAll('tag').forEach(tag => { const isLocked = tag.getAttribute('lock'); const text = tag.textContent; tags.push({text, isLocked}); }); return new VideoInfo(rawData); } constructor(rawData) { this._rawData = rawData; } get status() { return this._rawData.status; } get videoId() { return this._rawData.videoId; } get watchId() { return this._rawData.watchId; } get videoTitle() { return this._rawData.videoTitle; } get videoThumbnail() { return this._rawData.videoThumbnail; } get description() { return this._rawData.description; } get duration() { return this._rawData.duration; } get owner() { return { type: this.isChannel ? 'channel' : 'user', id: this.ownerId, name: this.ownerName, icon: this.ownerIcon }; } get ownerPageLink() { const ownerId = this.ownerId; if (this.isChannel) { return `${protocol}//ch.nicovideo.jp/ch${ownerId}`; } else { return `${protocol}//www.nicovideo.jp/user/${ownerId}`; } } get ownerIcon() { return this._rawData.ownerIcon; } get ownerName() { return this._rawData.ownerName; } get localeOwnerName() { if (this.isChannel) { return this.ownerName; } else { // TODO: 言語依存 return this.ownerName + ' さん'; } } get ownerId() { return this._rawData.ownerId; } get isChannel() { return this._rawData.isChannel; } get uploadDate() { return new Date(this._rawData.uploadDate); } get viewCounter() { return this._rawData.viewCounter; } get mylistCounter() { return this._rawData.mylistCounter; } get commentCounter() { return this._rawData.commentCounter; } get lastResBody() { return this._rawData.lastResBody; } get tags() { return this._rawData.tags; } } const deflistAdd = (watchId) => { const enableAutoComment = config.getValue('mylist.enableAutoComment'); if (location.host === 'www.nicovideo.jp') { if (enableAutoComment) { return DeflistApiLoader.addItemWithOwnerName(watchId); } else { return DeflistApiLoader.addItem(watchId, ''); } } let zenza; let token; return ZenzaDetector.detect().then((z) => { zenza = z; }).then(() => { return CsrfTokenLoader.load().then((t) => { token = t; }, () => { return Promise.resolve(); }); }).then(() => { if (!enableAutoComment) { return {}; } return ThumbInfoLoader.loadOwnerInfo(watchId); }).then((owner) => { //console.info(watchId, token, owner, zenza); if (!owner.id) { return zenza.external.deflistAdd({watchId}); } const description = owner.localeName; return zenza.external.deflistAdd({watchId, description, token}); }); }; const deflistRemove = (watchId) => { if (location.host === 'www.nicovideo.jp') { return DeflistApiLoader.removeItem(watchId); } let zenza; let token; return ZenzaDetector.detect().then((z) => { zenza = z; }).then(() => { return CsrfTokenLoader.load().then((t) => { token = t; }, () => { return Promise.resolve(); }); }).then(() => { return zenza.external.deflistRemove({watchId, token}); }); }; class MatchChecker { constructor({word = '', tag = '', owner = ''}) { this.init({word, tag, owner}); } init({word, tag, owner}) { this._tag = []; tag.split(/[\r\n]+/).forEach((t) => { if (t) { this._tag.push(t.trim()); } }); this._tag = _.uniq(this._tag); let wordTmp = []; this._word = null; word.split(/[\r\n]+/).forEach((w) => { if (w) { wordTmp.push(util.escapeRegs(w.trim())); } }); wordTmp = _.uniq(wordTmp); if (wordTmp.length > 0) { this._word = new RegExp('(' + wordTmp.join('|') + ')', 'i'); } //console.info('word', word, wordTmp.length, this._word); this._userId = []; this._channelId = []; owner.split(/[\r\n]+/).forEach((o) => { if (typeof o === 'string') { const id = o.split('#')[0].trim(); if (id.startsWith('ch')) { this._channelId.push(parseInt(id.substring(2))); } else { this._userId.push(parseInt(id)); } } }); this._userId = _.uniq(this._userId); this._channelId = _.uniq(this._channelId); //console.info('ng', this._tag, this._word, this._userId, this._channelId); } isMatch(data) { if (this._isMatchTag(data.tagList)) { return true; } if (this._isMatchOwner(data.owner)) { return true; } if (this._isMatchWord({title: data.title, description: data.description})) { return true; } } _isMatchTag(tagList = []) { if (this._tag.length < 1) { return false; } const tagTmp = []; tagList.forEach(t => { if (t) { tagTmp.push(util.escapeRegs(t.trim ? t.trim() : t.text.trim())); } }); const tagReg = new RegExp(' (' + tagTmp.join('|') + ') ', 'i'); const _tag = ' ' + this._tag.join(' ') + ' '; return tagReg.test(_tag); } _isMatchOwner(owner) { const _id = owner.type === 'user' ? this._userId : this._channelId; return _id.includes(parseInt(owner.id, 10)); } _isMatchWord({title, description}) { if (!this._word) { return false; } //console.log(title, this._word.test(title)); //console.log(description, this._word.test(description)); return this._word.test(title) || this._word.test(description); } isMatchTag(tag) { return this._isMatchTag([tag]); } isMatchOwner(owner) { return this._isMatchOwner(owner); } } class NgChecker extends MatchChecker { isNg(data) { return super.isMatch(data); } } const initDom = () => { util.addStyle(__css__); const f = document.createElement('div'); f.id = 'mylistPocketDomContainer'; f.innerHTML = __tpl__; document.body.appendChild(f); }; const initZenzaBridge = () => { ZenzaDetector.initialize(); }; const createVideoInfoView = () => { const host = document.getElementById('mylistPocket-popup'); const tpl = document.getElementById('mylistPocket-popup-template'); const vv = new VideoInfoView({host, tpl}); return vv; }; const createVideoInfoLoader = (vv) => { const onVideoInfoLoad = ({data, watchId}) => { const vi = VideoInfo.createByThumbInfo({xml: data, watchId}); vv.bind(vi); }; const onVideoInfoFail = () => { vv.bind({status: 'fail', description: '通信失敗'}); return Promise.resolve(); }; return function(watchId) { vv.reset(); vv.show(); return ThumbInfoLoader.loadXml(watchId).then( onVideoInfoLoad, onVideoInfoFail ); }; }; const createCommandDispatcher = ({infoView}) => { const info = createVideoInfoLoader(infoView); const ngConfig = config.namespace('ng'); const favConfig = config.namespace('fav'); const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig}); const toggleFavNg = (command, param) => { let [cmd, namespace, key] = command.split('-'); let _config = namespace === 'fav' ? favConfig : ngConfig; _config.refresh(); const value = param.value.trim(); let ngs = _config.getValue(key).trim().split(/[\r\n]/); const isContain = ngs.includes(value); if (isContain || cmd === 'remove') { ngs = ngs.filter((line) => { if (line === value) { window.console.info('%c-%s:%s', 'background: cyan', key, value); } return line !== value; }); cmd = 'remove'; } else if (!isContain || cmd === 'add') { ngs.push(value); window.console.info('%c+%s:%s', 'background: cyan', key, value); cmd = 'add'; } ngs = _.uniq(ngs); _config.setValue(key, ngs.join('\n').trim()); const className = namespace === 'fav' ? 'is-fav-favorited' : 'is-ng-rejected'; Array.prototype.forEach.call( document.querySelectorAll(`*[data-watch-id=${param.watchId}]`), item => { item.classList.toggle(className, cmd === 'add'); }); }; return (command, param, src) => { switch(command) { case 'info': return info(param); case 'load': return QueueLoader.load(param); case 'fav-status': return QueueLoader.load(param).then((result) => { if (!result || !result.data || result.data.status === 'fail' || result.data.code === 'DELETED') { return Promise.reject({status: 'unknown', result}); } if (ngChecker.isMatch(result.data)) { return {status: 'ng', result}; } if (favChecker.isMatch(result.data)) { return {status: 'favorite', result}; } return {status: 'default', result}; }); case 'mylist-window': window.open( protocol + '//www.nicovideo.jp/mylist_add/video/' + param, 'nicomylistadd', 'width=500, height=400, menubar=no, scrollbars=no'); break; case 'twitter-hash-open': window.open('https://twitter.com/hashtag/' + param + '?src=hash'); break; case 'open-mylist-open': window.open(protocol + '//www.nicovideo.jp/openlist/' + param); break; case 'mylist-comment-open': window.open(protocol + '//www.nicovideo.jp/mylistcomment/video/' + param); break; case 'zenza-open-now': if (window.ZenzaWatch.config.getValue('enableSingleton')) { window.ZenzaWatch.external.sendOrExecCommand('openNow', param); } else { window.ZenzaWatch.external.execCommand('openNow', param); } break; case 'zenza-open': if (window.ZenzaWatch.config.getValue('enableSingleton')) { window.ZenzaWatch.external.sendOrExecCommand('open', param); } else { window.ZenzaWatch.external.open(param); } break; case 'playlist-inert': window.ZenzaWatch.external.playlist.insert(param); break; case 'playlist-queue': window.ZenzaWatch.external.playlist.add(param); break; case 'deflist-add': src.notifyBeginDeflistUpdate('is-deflistUpdating'); return deflistAdd(param) .then(util.getSleepPromise(1000, 'deflist-add')) .then((result) => { src.notifyEndDeflistUpdate(result); }, (err) => { console.error('deflist-add-result', err); src.notifyFailDeflistUpdate(err); }); case 'deflist-remove': src.notifyBeginDeflistUpdate('is-deflistUpdating'); return deflistRemove(param) .then(util.getSleepPromise(1000, 'deflist-remove')) .then(() => { src.notifyEndDeflistUpdate({message: '削除しました'}); }, (err) => { console.error('deflist-remove-result', err); src.notifyFailDeflistUpdate(err); }); case 'add-ng-word': case 'add-ng-tag': case 'add-ng-owner': case 'add-fav-word': case 'add-fav-tag': case 'add-fav-owner': case 'remove-ng-word': case 'remove-ng-tag': case 'remove-ng-owner': case 'remove-fav-word': case 'remove-fav-tag': case 'remove-fav-owner': case 'toggle-ng-word': case 'toggle-ng-tag': case 'toggle-ng-owner': case 'toggle-fav-word': case 'toggle-fav-tag': case 'toggle-fav-owner': toggleFavNg(command, param); break; } }; }; const initExternal = (dispatcher, hoverMenu, infoView) => { MylistPocket.external = { info: watchId => { return dispatcher('info', watchId); }, load: watchId => { return dispatcher('load', watchId); }, getFavStatus: (watchId) => { return dispatcher('fav-status', watchId); }, observe: (params /*{query, container, closest}*/) => { initNg(params); }, hide: () => { hoverMenu.hide(); infoView.hide(); } }; MylistPocket.isReady = true; const ev = new CustomEvent('MylistPocketInitialized', { detail: { MylistPocket } }); document.body.dispatchEvent(ev); // 過去の互換用 if (window.jQuery) { window.jQuery('body').trigger('MylistPocketReady', MylistPocket); } }; const QueueLoader = (() => { let lastPromise = null; let count = 0; const MAX_LOAD = 6; const promises = []; const load = function(watchId, item) { count = (count + 1) % MAX_LOAD; lastPromise = promises[count]; //console.time('load-' + watchId); const onLoad = (result) => { //console.timeEnd('load-' + watchId); if (item) { watchId = (result.data.id || '').startsWith('so') ? result.data.id : watchId; item.setAttribute('data-watch-id', watchId); item.setAttribute('data-thumb-info', JSON.stringify(result)); } const sleepTime = result.data.fromCache ? 0 : 50; return (util.getSleepPromise(sleepTime, 'success-' + watchId))(result); }; const onFail = util.getSleepPromise(1000, 'fail-' + watchId); if (!lastPromise) { if (item) { item.classList.add('is-ng-current'); } lastPromise = ThumbInfoLoader.load(watchId).then(onLoad, onFail); } else { //lastPromise = Promise.all([lastPromise]).then(() => { lastPromise = Promise.race(promises).then(() => { if (item) { item.classList.add('is-ng-current'); } return ThumbInfoLoader.load(watchId).then(onLoad, onFail); }); } promises[count] = lastPromise; return lastPromise; }; return { load }; })(); const getNgEnv = () => { if (location.host === 'www.nicovideo.jp' && (location.pathname.startsWith('/ranking') || location.pathname.startsWith('/tag') || location.pathname.startsWith('/search')) ) { return { query: '.item[data-video-id]:not(.is-ng-wait), .item_cell[data-video-id]:not(.is-ng-wait)', container: document.querySelector('.column.main, .container.column1024-0') }; } if (location.host === 'www.nicovideo.jp' && document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp')) { return { query: 'a[href*="/watch/"]:not(.is-ng-wait)', container: document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp'), closest: '.log' }; } if (location.host === 'ch.nicovideo.jp' && location.pathname.startsWith('/search')) { return { query: '.item:not(.is-ng-wait)', container: document.querySelector('.site_body') }; } if (location.host === 'search.nicovideo.jp') { return { query: '.video:not(.is-ng-wait)', container: document.querySelector('#row-results') }; } return {query: null, container: null}; }; const initNgConfig = () => { const ngConfig = config.namespace('ng'); const updateEnable = v => { document.body.classList.toggle('is-ng-disable', !v); }; updateEnable(ngConfig.getValue('enable')); if (!ngConfig.getValue('enable')) { return {}; } ngConfig.on('enable', updateEnable); const favConfig = config.namespace('fav'); return {ngConfig, favConfig}; }; const initNgChecker = ({ngConfig, favConfig}) => { const ngChecker = new NgChecker({ word: ngConfig.getValue('word'), tag: ngConfig.getValue('tag'), owner: ngConfig.getValue('owner') }); ngConfig.on('@update', _.debounce(({key, value}) => { //console.info('ngConfig updated: ', key, value); ngChecker.init({ word: ngConfig.getValue('word'), tag: ngConfig.getValue('tag'), owner: ngConfig.getValue('owner') }); }, 100)); const favChecker = new MatchChecker({ word: favConfig.getValue('word'), tag: favConfig.getValue('tag'), owner: favConfig.getValue('owner') }); favConfig.on('@update', _.debounce(({key, value}) => { //console.info('favConfig updated: ', key, value); favChecker.init({ word: favConfig.getValue('word'), tag: favConfig.getValue('tag'), owner: favConfig.getValue('owner') }); }, 100)); return {ngChecker, favChecker}; }; const initIntersectionObserver = (onInview) => { const onItemInview = item => { let watchId = item.getAttribute('data-id') || item.getAttribute('data-video-id') || item.getAttribute('data-watch-id'); const ignore = () => { item.classList.add('is-ng-ignore'); }; if (!watchId) { const a = item.querySelector("a[href*='watch/']"); if (!a) { return ignore(); } if (a.hostname !== 'www.nicovideo.jp') { return ignore(); } if (!/^\/watch\/([a-z0-9]+)/.test(a.pathname)) { return ignore(); } watchId = RegExp.$1; } if (!watchId) { return ignore(); } item.classList.add('is-ng-queue'); onInview(item, watchId); }; const intersectionObserver = new window.IntersectionObserver(entries => { entries.filter(entry => entry.isIntersecting).forEach(entry => { const item = entry.target; intersectionObserver.unobserve(item); onItemInview(item); }); }, { rootMargin: '400px'}); return intersectionObserver; }; const initNgDom = ({intersectionObserver, query, closest, container}) => { if (!container) { return; } util.addStyle(__ng_css__); const update = () => { let items = (container || document).querySelectorAll(query); if (!items || items.length < 1) { return; } if (closest) { let tmp = []; Array.from(items).forEach(item => { const c = item.closest(closest); if (c && !tmp.includes(c)) { tmp.push(c); } }); items = tmp; } if (!items || items.length < 1) { return; } Array.from(items).forEach(item => { //if (item.offsetLeft < 0) { return; } if (item.classList.contains('is-ng-ignore')) { return; } item.classList.add('is-ng-wait'); intersectionObserver.observe(item); }); }; update(); const onUpdate = _.throttle(update, 1000); if (!container) { return; } const mutationObserver = new window.MutationObserver((mutations) => { let isAdded = false; mutations.forEach(mutation => { if (mutation.addedNodes && mutation.addedNodes.length > 0) { isAdded = true; } }); if (isAdded) { onUpdate(); } }); mutationObserver.observe( container, {childList: true, characterData: false, attributes: false, subtree: true} ); }; const initNg = params => { if (!window.IntersectionObserver) { return; } let {query, container, closest} = params ? params : getNgEnv(); if (!query) { return; } const {ngConfig, favConfig} = initNgConfig(); if (!ngConfig) { return; } const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig}); const onItemInview = (item, watchId) => { const loadLazy = () => { const lazyImage = item.querySelector('.jsLazyImage'); if (lazyImage) { const origImage = lazyImage.getAttribute('data-original'); if (origImage) { lazyImage.src = origImage; lazyImage.classList.remove('jsLazyImage'); } } }; QueueLoader.load(watchId, item).then( (result) => { item.classList.remove('is-ng-current'); if (!result || !result.data || result.data.status === 'fail' || result.data.code === 'DELETED') { if (result && result.data && result.data.code !== 'COMMUNITY') { console.error('empty data', watchId, result, result.data ? result.data.code : 'unknown'); } item.classList.add('is-ng-failed', (result && result.data) ? result.data.code : 'is-no-data'); } else { item.classList.add( ngChecker.isNg(result.data) ? 'is-ng-rejected' : 'is-ng-resolved'); if (favChecker.isMatch(result.data)) { item.classList.add('is-fav-favorited'); } // チャンネル動画のリンクを watch/so〜 に置き換える if (!(result.data.id || '').startsWith('so')) { return; } for (let a of item.querySelectorAll(`a[href*="watch/${watchId}"]`)) { let href = a.getAttribute('href'); href = href.replace(/watch\/([0-9]+)/, `watch/${result.data.id}`); a.setAttribute('href', href); } } loadLazy(); }, () => { item.classList.remove('is-ng-current'); item.classList.add('is-ng-failed'); loadLazy(); } ); }; const intersectionObserver = initIntersectionObserver(onItemInview); initNgDom({intersectionObserver, query, container, closest}); }; const init = () => { initDom(); initZenzaBridge(); const infoView = createVideoInfoView(); const dispatcher = createCommandDispatcher({infoView}); infoView.on('command', dispatcher); const hoverMenu = new HoverMenu(); hoverMenu.on('info', (watchId) => { hoverMenu.isBusy = true; dispatcher('info', watchId) .then(() => { hoverMenu.isBusy = false; }); }); hoverMenu.on('deflist-add', (watchId, src) => { dispatcher('deflist-add', watchId, src); }); hoverMenu.on('deflist-remove', (watchId, src) => { dispatcher('deflist-remove', watchId, src); }); hoverMenu.on('playlist-queue', (watchId, src) => { dispatcher('playlist-queue', watchId, src); }); MylistPocket.debug.hoverMenu = hoverMenu; initNg(); if (config.getValue('nicoad.hide')) { util.addStyle(nicoadHideCss); } initExternal(dispatcher, hoverMenu, infoView); }; init(); }; const postToParent = function(type, message, token) { const origin = document.referrer; //console.info('postToParent type=%s, message=%s, token=%s, origin=%s', // type, message, token, origin); try { parent.postMessage(JSON.stringify({ id: PRODUCT, type: type, body: { token: token, url: location.href, message: message } }), origin); } catch (e) { alert(e); console.log('err', e); } }; const parseUrl = (url) => { const a = document.createElement('a'); a.href = url; return a; }; const HOST_REG = /^([a-z0-9]*\.nicovideo\.jp|www\.google\.com|www\.google\.co\.jp)$/; const thumbInfoApi = function() { if (window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') < 0 ) { return; } window.console.log( '%cCrossDomainGate: %s %s', 'background: lightgreen;', PRODUCT, location.host); if (!HOST_REG.test(parseUrl(document.referrer).hostname)) { window.console.log('disable bridge'); return; } const type = 'thumbInfo' + PRODUCT; const token = location.hash ? location.hash.substr(1) : null; location.hash = ''; window.addEventListener('message', (event) => { const data = JSON.parse(event.data); let timeoutTimer, isTimeout = false; if (!HOST_REG.test(parseUrl(event.origin).hostname)) { return; } if (data.token !== token) { return; } //window.console.log('child onMessage', data, event); if (!data.url) { return; } const u = parseUrl(data.url); if (!u.hostname.startsWith('ext.nicovideo.jp') || !u.pathname.startsWith('/api/getthumbinfo/')) { return; } const sessionId = data.sessionId; fetch(data.url).then((resp) => { return resp.text(); }).then((text) => { if (isTimeout) { return; } else { window.clearTimeout(timeoutTimer); } try { postToParent(type, { sessionId: sessionId, status: 'ok', token: token, url: data.url, body: text }); } catch (e) { console.log( '%cError: parent.postMessage - ', 'color: red; background: yellow', e, event.origin, event.data); } }); timeoutTimer = window.setTimeout(() => { isTimeout = true; postToParent(type, { sessionId: sessionId, status: 'timeout', command: 'loadUrl', url: data.url }); }, 30000); }); try { postToParent(type, { status: 'initialized' }); } catch (e) { console.log('err', e); } }; const loadGm = function() { const script = document.createElement('script'); script.id = `${PRODUCT}Loader`; script.setAttribute('type', 'text/javascript'); script.setAttribute('charset', 'UTF-8'); script.appendChild(document.createTextNode( '(' + monkey + ')("' + PRODUCT + '");' )); document.body.appendChild(script); }; const host = window.location.host || ''; //const href = (location.href || '').replace(/#.*$/, ''); //const prot = location.protocol; if (host === 'ext.nicovideo.jp' && window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') >= 0) { thumbInfoApi(); } else if (window === top) { loadGm(); } })();