ニコニコ動画のランキングとキーワード・タグ検索結果に NG 機能を追加
// ==UserScript== // @name Nico Nico Ranking NG // @namespace http://userscripts.org/users/121129 // @description ニコニコ動画のランキングとキーワード・タグ検索結果に NG 機能を追加 // @match *://www.nicovideo.jp/ranking/genre/* // @match *://www.nicovideo.jp/ranking/hot_topic* // @match *://www.nicovideo.jp/search/* // @match *://www.nicovideo.jp/tag/* // @version 59 // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM.getValue // @grant GM.setValue // @grant GM.xmlHttpRequest // @grant GM.openInTab // @license MIT License // @noframes // @run-at document-start // @connect ext.nicovideo.jp // ==/UserScript== // https://d3js.org/d3-dsv/ Version 1.0.0. Copyright 2016 Mike Bostock. ;(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.d3 = global.d3 || {}))); }(this, function (exports) { 'use strict'; function objectConverter(columns) { return new Function("d", "return {" + columns.map(function(name, i) { return JSON.stringify(name) + ": d[" + i + "]"; }).join(",") + "}"); } function customConverter(columns, f) { var object = objectConverter(columns); return function(row, i) { return f(object(row), i, columns); }; } // Compute unique columns in order of discovery. function inferColumns(rows) { var columnSet = Object.create(null), columns = []; rows.forEach(function(row) { for (var column in row) { if (!(column in columnSet)) { columns.push(columnSet[column] = column); } } }); return columns; } function dsv(delimiter) { var reFormat = new RegExp("[\"" + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); function parse(text, f) { var convert, columns, rows = parseRows(text, function(row, i) { if (convert) return convert(row, i - 1); columns = row, convert = f ? customConverter(row, f) : objectConverter(row); }); rows.columns = columns; return rows; } function parseRows(text, f) { var EOL = {}, // sentinel value for end-of-line EOF = {}, // sentinel value for end-of-file rows = [], // output rows N = text.length, I = 0, // current character index n = 0, // the current line number t, // the current token eol; // is the current token followed by EOL? function token() { if (I >= N) return EOF; // special case: end of file if (eol) return eol = false, EOL; // special case: end of line // special case: quotes var j = I, c; if (text.charCodeAt(j) === 34) { var i = j; while (i++ < N) { if (text.charCodeAt(i) === 34) { if (text.charCodeAt(i + 1) !== 34) break; ++i; } } I = i + 2; c = text.charCodeAt(i + 1); if (c === 13) { eol = true; if (text.charCodeAt(i + 2) === 10) ++I; } else if (c === 10) { eol = true; } return text.slice(j + 1, i).replace(/""/g, "\""); } // common case: find next delimiter or newline while (I < N) { var k = 1; c = text.charCodeAt(I++); if (c === 10) eol = true; // \n else if (c === 13) { eol = true; if (text.charCodeAt(I) === 10) ++I, ++k; } // \r|\r\n else if (c !== delimiterCode) continue; return text.slice(j, I - k); } // special case: last token before EOF return text.slice(j); } while ((t = token()) !== EOF) { var a = []; while (t !== EOL && t !== EOF) { a.push(t); t = token(); } if (f && (a = f(a, n++)) == null) continue; rows.push(a); } return rows; } function format(rows, columns) { if (columns == null) columns = inferColumns(rows); return [columns.map(formatValue).join(delimiter)].concat(rows.map(function(row) { return columns.map(function(column) { return formatValue(row[column]); }).join(delimiter); })).join("\n"); } function formatRows(rows) { return rows.map(formatRow).join("\n"); } function formatRow(row) { return row.map(formatValue).join(delimiter); } function formatValue(text) { return text == null ? "" : reFormat.test(text += "") ? "\"" + text.replace(/"/g, "\"\"") + "\"" : text; } return { parse: parse, parseRows: parseRows, format: format, formatRows: formatRows }; } var csv = dsv(","); var csvParse = csv.parse; var csvParseRows = csv.parseRows; var csvFormat = csv.format; var csvFormatRows = csv.formatRows; var tsv = dsv("\t"); var tsvParse = tsv.parse; var tsvParseRows = tsv.parseRows; var tsvFormat = tsv.format; var tsvFormatRows = tsv.formatRows; exports.dsvFormat = dsv; exports.csvParse = csvParse; exports.csvParseRows = csvParseRows; exports.csvFormat = csvFormat; exports.csvFormatRows = csvFormatRows; exports.tsvParse = tsvParse; exports.tsvParseRows = tsvParseRows; exports.tsvFormat = tsvFormat; exports.tsvFormatRows = tsvFormatRows; Object.defineProperty(exports, '__esModule', { value: true }); })); ;(function() { 'use strict' var createObject = function(prototype, properties) { var descriptors = function() { return Object.keys(properties).reduce(function(descriptors, key) { descriptors[key] = Object.getOwnPropertyDescriptor(properties, key) return descriptors }, {}) } return Object.defineProperties(Object.create(prototype), descriptors()) } var set = function(target, propertyName) { return function(value) { target[propertyName] = value } } var movieIdOf = function(absoluteMovieURL) { return new URL(absoluteMovieURL).pathname.slice('/watch/'.length) } const ancestor = (child, selector) => { for (let n = child.parentNode; n; n = n.parentNode) if (n.matches(selector)) return n return null } var EventEmitter = (function() { var EventEmitter = function() { this._eventNameToListeners = new Map() } EventEmitter.prototype = { on(eventName, listener) { var m = this._eventNameToListeners var v = m.get(eventName) if (v) { v.add(listener) } else { m.set(eventName, new Set([listener])) } return this }, emit(eventName) { var m = this._eventNameToListeners var args = Array.from(arguments).slice(1) for (var l of m.get(eventName) || []) l(...args) }, off(eventName, listener) { var v = this._eventNameToListeners.get(eventName) if (v) v.delete(listener) }, } return EventEmitter })() var Listeners = (function() { var Listeners = function(eventNameToListener) { this.eventNameToListener = eventNameToListener this.eventEmitter = null } Listeners.prototype = { bind(eventEmitter) { this.eventEmitter = eventEmitter Object.keys(this.eventNameToListener).forEach(function(k) { eventEmitter.on(k, this.eventNameToListener[k]) }, this) }, unbind() { if (!this.eventEmitter) return Object.keys(this.eventNameToListener).forEach(function(k) { this.eventEmitter.off(k, this.eventNameToListener[k]) }, this) }, } return Listeners })() var ArrayStore = (function(_super) { var isObject = function(v) { return v === Object(v) } var valueIfObj = function(v) { return isObject(v) ? v.value : v } var toUpperCase = function(s) { return s.toUpperCase() } var ArrayStore = function(getValue, setValue, key, caseInsensitive) { _super.call(this) this.getValue = getValue this.setValue = setValue this.key = key this.caseInsensitive = Boolean(caseInsensitive) this._arrayWithText = [] } ArrayStore.prototype = createObject(_super.prototype, { get array() { return this.arrayWithText.map(valueIfObj) }, get arrayWithText() { return this._arrayWithText }, _setOf(values) { return new Set(this.caseInsensitive ? values.map(toUpperCase) : values) }, get set() { return this._setOf(this.array) }, _toUpperCaseIfRequired(value) { return this.caseInsensitive ? value.toUpperCase() : value }, _concat(value, text) { return this.arrayWithText.concat(text ? {value, text} : value) }, add(value, text) { if (this.set.has(this._toUpperCaseIfRequired(value))) return false this.arrayWithText.push(text ? {value, text} : value) this.setValue(this.key, JSON.stringify(this.arrayWithText)) this.emit('changed', this.set) return true }, async addAsync(value, text) { await this.sync() return this.add(value, text) }, addAll(values) { if (values.length === 0) return var oldVals = this.arrayWithText var set = this._setOf(oldVals.map(valueIfObj)) var filtered = values.filter(function(v) { return !set.has(this._toUpperCaseIfRequired(valueIfObj(v))) }, this) if (filtered.length === 0) return this.arrayWithText.push(...filtered) this.setValue(this.key, JSON.stringify(this.arrayWithText)) this.emit('changed', this.set) }, _reject(values) { var valueSet = this._setOf(values) return this.arrayWithText.filter(function(v) { return !valueSet.has(this._toUpperCaseIfRequired(valueIfObj(v))) }, this) }, remove(values) { const oldVals = this.arrayWithText const newVals = this._reject(values) if (oldVals.length === newVals.length) return this._arrayWithText = newVals this.setValue(this.key, JSON.stringify(newVals)) this.emit('changed', this.set) }, async removeAsync(values) { await this.sync() this.remove(values) }, clear() { if (!this.arrayWithText.length) return this._arrayWithText = [] this.setValue(this.key, '[]') this.emit('changed', new Set()) }, async sync() { this._arrayWithText = JSON.parse(await this.getValue(this.key, '[]')) }, }) return ArrayStore })(EventEmitter) var Store = (function(_super) { var Store = function(getValue, setValue, key, defaultValue) { _super.call(this) this.getValue = getValue this.setValue = setValue this.key = key this._value = this.defaultValue = defaultValue } Store.prototype = createObject(_super.prototype, { get value() { return this._value }, set value(value) { if (this._value === value) return this._value = value this.setValue(this.key, value) this.emit('changed', value) }, async sync() { this._value = await this.getValue(this.key, this.defaultValue) } }) return Store })(EventEmitter) var Config = (function() { var ngMovieVisibleStore = function() { var value var getValue = function(_, defval) { return value === undefined ? defval : value } var setValue = function(_, v) { value = v } return new Store(getValue, setValue, 'ngMovieVisible', false) } var csv = (function() { var RECORD_LENGTH = 3 var TYPE = 0 var VALUE = 1 var TEXT = 2 var isObject = function(v) { return v === Object(v) } var csvToArray = function(csv) { /* パース対象の文字列最後の文字がカンマのとき、 そのカンマが空のフィールドとしてパースされない。 \n を追加して対処する。 */ return d3.csvParseRows(csv + '\n') } var createRecord = function(type, value, text) { var r###lt = [] r###lt[TYPE] = type r###lt[VALUE] = value r###lt[TEXT] = text return r###lt } var trimFields = function(record) { var r = record return createRecord(r[TYPE].trim(), r[VALUE].trim(), r[TEXT].trim()) } var isIntValueType = function(type) { return ['ngUserId', 'ngChannelId'].indexOf(type) >= 0 } var hasValidValue = function(record) { var v = record[VALUE] return v.length !== 0 && !(isIntValueType(record[TYPE]) && Number.isNaN(Math.trunc(v))) } var valueToIntIfIntValueType = function(record) { var r = record return isIntValueType(r[TYPE]) ? createRecord(r[TYPE], Math.trunc(r[VALUE]), r[TEXT]) : r } var records = function(csv) { return csvToArray(csv) .filter(function(record) { return record.length === RECORD_LENGTH }) .map(trimFields) .filter(hasValidValue) .map(valueToIntIfIntValueType) } var isValueOnlyType = function(type) { return ['ngTitle', 'ngTag', 'ngUserName'].indexOf(type) >= 0 } var getValue = function(record) { var value = record[VALUE] if (isValueOnlyType(record[TYPE])) return value var text = record[TEXT] return text ? {value, text} : value } var createTypeToValuesMap = function() { return new Map([ ['ngMovieId', []], ['ngTitle', []], ['ngTag', []], ['ngUserId', []], ['ngUserName', []], ['ngChannelId', []], ['visitedMovieId', []], ]) } return { create(arrayStore, type) { return d3.csvFormatRows(arrayStore.arrayWithText.map(function(value) { return isObject(value) ? createRecord(type, value.value, value.text) : createRecord(type, value, '') })) }, parse(csv) { var r###lt = createTypeToValuesMap() for (var r of records(csv)) { var values = r###lt.get(r[TYPE]) if (values) values.push(getValue(r)) } return r###lt }, } })() var Config = function(getValue, setValue) { var store = function(key, defaultValue) { return new Store(getValue, setValue, key, defaultValue) } var arrayStore = function(key, caseInsensitive) { return new ArrayStore(getValue, setValue, key, caseInsensitive) } this.visitedMovieViewMode = store('visitedMovieViewMode', 'reduce') this.visibleContributorType = store('visibleContributorType', 'all') this.openNewWindow = store('openNewWindow', true) this.useGetThumbInfo = store('useGetThumbInfo', true) this.movieInfoTogglable = store('movieInfoTogglable', true) this.descriptionTogglable = store('descriptionTogglable', true) this.visitedMovies = arrayStore('visitedMovies') this.ngMovies = arrayStore('ngMovies') this.ngTitles = arrayStore('ngTitles', true) this.ngTags = arrayStore('ngTags', true) this.ngLockedTags = arrayStore('ngLockedTags', true) this.ngUserIds = arrayStore('ngUserIds') this.ngUserNames = arrayStore('ngUserNames', true) this.ngChannelIds = arrayStore('ngChannelIds') this.ngMovieVisible = ngMovieVisibleStore() this.addToNgLockedTags = store('addToNgLockedTags', false); } Config.prototype.sync = function() { return Promise.all([ this.visitedMovieViewMode.sync(), this.visibleContributorType.sync(), this.openNewWindow.sync(), this.useGetThumbInfo.sync(), this.movieInfoTogglable.sync(), this.descriptionTogglable.sync(), this.visitedMovies.sync(), this.ngMovies.sync(), this.ngTitles.sync(), this.ngTags.sync(), this.ngLockedTags.sync(), this.ngUserIds.sync(), this.ngUserNames.sync(), this.ngChannelIds.sync(), this.addToNgLockedTags.sync(), ]) } Config.prototype.toCSV = async function(targetTypes) { await this.sync() var csvTexts = [] if (targetTypes['ngMovieId']) { csvTexts.push(csv.create(this.ngMovies, 'ngMovieId')) } if (targetTypes['ngTitle']) { csvTexts.push(csv.create(this.ngTitles, 'ngTitle')) } if (targetTypes['ngTag']) { csvTexts.push(csv.create(this.ngTags, 'ngTag')) } if (targetTypes['ngUserId']) { csvTexts.push(csv.create(this.ngUserIds, 'ngUserId')) } if (targetTypes['ngUserName']) { csvTexts.push(csv.create(this.ngUserNames, 'ngUserName')) } if (targetTypes['ngChannelId']) { csvTexts.push(csv.create(this.ngChannelIds, 'ngChannelId')) } if (targetTypes['visitedMovieId']) { csvTexts.push(csv.create(this.visitedMovies, 'visitedMovieId')) } return csvTexts.filter(Boolean).join('\n') } Config.prototype.addFromCSV = async function(csvText) { await this.sync() var map = csv.parse(csvText) this.ngMovies.addAll(map.get('ngMovieId')) this.ngTitles.addAll(map.get('ngTitle')) this.ngTags.addAll(map.get('ngTag')) this.ngUserIds.addAll(map.get('ngUserId')) this.ngUserNames.addAll(map.get('ngUserName')) this.ngChannelIds.addAll(map.get('ngChannelId')) this.visitedMovies.addAll(map.get('visitedMovieId')) } return Config })() var ThumbInfo = (function(_super) { const parseTags = tags => { return Array.from(tags, tag => { return { name: tag.textContent, lock: tag.getAttribute('lock') === '1', }; }); }; var contributor = function(rootElem, type, id, name) { return { type: type, id: parseInt(rootElem.querySelector(id).textContent), name: rootElem.querySelector(name)?.textContent ?? '', } } var user = function(rootElem) { return contributor(rootElem , 'user' , 'thumb > user_id' , 'thumb > user_nickname') } var channel = function(rootElem) { return contributor(rootElem , 'channel' , 'thumb > ch_id' , 'thumb > ch_name') } var parseContributor = function(rootElem) { var userId = rootElem.querySelector('thumb > user_id') return userId ? user(rootElem) : channel(rootElem) } var parseThumbInfo = function(rootElem) { return { description: rootElem.querySelector('thumb > description').textContent, tags: parseTags(rootElem.querySelectorAll('thumb > tags > tag')), contributor: parseContributor(rootElem), title: rootElem.querySelector('thumb > title').textContent, error: {type: 'NO_ERROR', message: 'no error'}, } } var error = function(type, message, id) { var r###lt = {error: {type, message}} if (id) r###lt.id = id return r###lt } var parseError = function(rootElem) { var type = rootElem.querySelector('error > code').textContent switch (type) { case 'DELETED': return error(type, '削除された動画') case 'NOT_FOUND': return error(type, '見つからない、または無効な動画') case 'COMMUNITY': return error(type, 'コミュニティ限定動画') default: return error(type, 'エラーコード: ' + type) } } var parseResText = function(resText) { try { var d = new DOMParser().parseFromString(resText, 'application/xml') var r = d.documentElement var status = r.getAttribute('status') switch (status) { case 'ok': return parseThumbInfo(r) case 'fail': return parseError(r) default: return error(status, 'ステータス: ' + status) } } catch (e) { return error('PARSING', 'パースエラー') } } var statusMessage = function(res) { return res.status + ' ' + res.statusText } var ThumbInfo = function(httpRequest, concurrent) { _super.call(this) this.httpRequest = httpRequest this.concurrent = concurrent || 5 this._requestCount = 0 this._pendingIds = [] this._requestedIds = new Set() } ThumbInfo.prototype = createObject(_super.prototype, { _onerror(id) { this._requestCount-- this._requestNextMovie() this.emit('errorOccurred', error('ERROR', 'エラー', id)) }, _ontimeout(id, retried) { if (retried) { this._requestCount-- this._requestNextMovie() this.emit('errorOccurred', error('TIMEOUT', 'タイムアウト', id)) } else { this._requestMovie(id, true) } }, _onload(id, res) { this._requestCount-- this._requestNextMovie() if (res.status === 200) { var thumbInfo = parseResText(res.responseText) thumbInfo.id = id if (thumbInfo.error.type === 'NO_ERROR') { this.emit('completed', thumbInfo) } else { this.emit('errorOccurred', thumbInfo) } } else { this.emit('errorOccurred' , error('HTTP_STATUS', statusMessage(res), id)) } }, _requestMovie(id, retry) { this.httpRequest({ method: 'GET', url: 'https://ext.nicovideo.jp/api/getthumbinfo/' + id, timeout: 5000, onload: this._onload.bind(this, id), onerror: this._onerror.bind(this, id), ontimeout: this._ontimeout.bind(this, id, retry), }) }, _requestNextMovie() { var id = this._pendingIds.shift() if (!id) return this._requestMovie(id) this._requestCount++ }, _getNewIds(ids) { ids = ids || [] var m = this._requestedIds return [...new Set(ids)].filter(function(id) { return !m.has(id) }) }, _requestAsPossible() { var space = this.concurrent - this._requestCount var c = Math.min(this._pendingIds.length, space) for (var i = 0; i < c; i++) this._requestNextMovie() }, request(ids, prefer) { const newIds = this._getNewIds(ids) for (const id of newIds) this._requestedIds.add(id) if (prefer) { for (const id of newIds) this._requestMovie(id) return } ;[].push.apply(this._pendingIds, newIds) this._requestAsPossible() return this }, }) return ThumbInfo })(EventEmitter) var Tag = (function(_super) { var Tag = function(thumbInfoTabObj) { _super.call(this); this.name = thumbInfoTabObj.name; this.lock = thumbInfoTabObj.lock; this.ngByNormal = false; this.ngByLock = false; } Tag.prototype = createObject(_super.prototype, { get ng() { return this.ngByNormal || this.ngByLock; }, updateNg(upperCaseNgTagNameSet) { var pre = this.ng this.ngByNormal = upperCaseNgTagNameSet.has(this.name.toUpperCase()) if (pre !== this.ng) this.emit('ngChanged', this.ng) }, updateNgIfLocked(upperCaseNgTagNameSet) { if (!this.lock) return; const pre = this.ng; this.ngByLock = upperCaseNgTagNameSet.has(this.name.toUpperCase()); if (pre !== this.ng) this.emit('ngChanged', this.ng); }, }) return Tag })(EventEmitter) var Contributor = (function(_super) { var Contributor = function(type, id, name) { _super.call(this) this.type = type this.id = id this.name = name this.ng = false this.ngId = false this.ngName = '' } Contributor.prototype = createObject(_super.prototype, { _updateNg() { var pre = this.ng this.ng = this.ngId || Boolean(this.ngName) if (pre !== this.ng) this.emit('ngChanged', this.ng) }, updateNgId(ngIdSet) { var pre = this.ngId this.ngId = ngIdSet.has(this.id) if (pre !== this.ngId) this.emit('ngIdChanged', this.ngId) this._updateNg() }, _getNewNgName(upperCaseNgNameSet) { var n = this.name.toUpperCase() for (var ngName of upperCaseNgNameSet) if (n.includes(ngName)) return ngName return '' }, updateNgName(upperCaseNgNameSet) { var pre = this.ngName this.ngName = this._getNewNgName(upperCaseNgNameSet) if (pre !== this.ngName) this.emit('ngNameChanged', this.ngName) this._updateNg() }, get url() { throw new Error('must be implemented') }, bindToConfig(config) { this.updateNgId(config[this.ngIdStoreName].set) config[this.ngIdStoreName].on('changed', this.updateNgId.bind(this)) }, }) var User = function(id, name) { Contributor.call(this, 'user', id, name) } User.prototype = createObject(Contributor.prototype, { get ngIdStoreName() { return 'ngUserIds' }, get url() { return 'https://www.nicovideo.jp/user/' + this.id }, bindToConfig(config) { Contributor.prototype.bindToConfig.call(this, config) this.updateNgName(config.ngUserNames.set) config.ngUserNames.on('changed', this.updateNgName.bind(this)) }, }) var Channel = function(id, name) { Contributor.call(this, 'channel', id, name) } Channel.prototype = createObject(Contributor.prototype, { get ngIdStoreName() { return 'ngChannelIds' }, get url() { return 'https://ch.nicovideo.jp/channel/ch' + this.id }, }) Object.assign(Contributor, { NULL: new Contributor('unknown', -1, ''), TYPES: ['user', 'channel'], new(type, id, name) { switch (type) { case 'user': return new User(id, name) case 'channel': return new Channel(id, name) default: throw new Error(type) } }, }) return Contributor })(EventEmitter) var Movie = (function(_super) { var Movie = function(id, title) { _super.call(this) this.id = id this.title = title this.ngTitle = '' this.ngId = false this.visited = false this._tags = [] this._contributor = Contributor.NULL this._description = '' this._error = Movie.NO_ERROR this._thumbInfoDone = false this._ng = false } Movie.NO_ERROR = {type: 'NO_ERROR', message: 'no error'} Movie.prototype = createObject(_super.prototype, { _matchedNgTitle(upperCaseNgTitleSet) { var t = this.title.toUpperCase() for (var ng of upperCaseNgTitleSet) { if (t.includes(ng)) return ng } return '' }, updateNgTitle(upperCaseNgTitleSet) { var pre = this.ngTitle this.ngTitle = this._matchedNgTitle(upperCaseNgTitleSet) if (pre === this.ngTitle) return this.emit('ngTitleChanged', this.ngTitle) this._updateNg() }, updateNgId(ngIdSet) { var pre = this.ngId this.ngId = ngIdSet.has(this.id) if (pre === this.ngId) return this.emit('ngIdChanged', this.ngId) this._updateNg() }, updateVisited(visitedIdSet) { var pre = this.visited this.visited = visitedIdSet.has(this.id) if (pre !== this.visited) this.emit('visitedChanged', this.visited) }, get description() { return this._description }, set description(description) { this._description = description this.emit('descriptionChanged', this._description) }, get tags() { return this._tags }, set tags(tags) { this._tags = tags this.emit('tagsChanged', this._tags) this._updateNg() var update = this._updateNg.bind(this) for (var t of this._tags) t.on('ngChanged', update) }, get contributor() { return this._contributor }, set contributor(contributor) { this._contributor = contributor this.emit('contributorChanged', this._contributor) this._updateNg() this._contributor.on('ngChanged', this._updateNg.bind(this)) }, get error() { return this._error }, set error(error) { this._error = error this.emit('errorChanged', this._error) }, get thumbInfoDone() { return this._thumbInfoDone }, setThumbInfoDone() { this._thumbInfoDone = true this.emit('thumbInfoDone') }, get ng() { return this._ng }, _updateNg() { var pre = this._ng this._ng = this.ngId || Boolean(this.ngTitle) || this.contributor.ng || this.tags.some(function(t) { return t.ng }) if (pre !== this._ng) this.emit('ngChanged', this._ng) }, addListenerToConfig(config) { config.ngMovies.on('changed', this.updateNgId.bind(this)) config.ngTitles.on('changed', this.updateNgTitle.bind(this)) config.visitedMovies.on('changed', this.updateVisited.bind(this)) }, }) return Movie })(EventEmitter) var Movies = (function() { var Movies = function(config) { this.config = config this._idToMovie = new Map() } Movies.prototype = { setIfAbsent(movies) { var ngIds = this.config.ngMovies.set var ngTitles = this.config.ngTitles.set var visitedIds = this.config.visitedMovies.set var map = this._idToMovie for (var m of movies) { if (map.has(m.id)) continue map.set(m.id, m) m.updateNgId(ngIds) m.updateNgTitle(ngTitles) m.updateVisited(visitedIds) m.addListenerToConfig(this.config) } }, get(movieId) { return this._idToMovie.get(movieId) }, } return Movies })() var ThumbInfoListener = (function() { var createTagBuilder = function(config) { var map = new Map() return thumbInfoTag => { let a; const i = thumbInfoTag.lock ? 1 : 0; if (map.has(thumbInfoTag.name)) { a = map.get(thumbInfoTag.name); if (a[i]) return a[i]; } else { a = [null, null]; } const tag = new Tag(thumbInfoTag); a[i] = tag; map.set(thumbInfoTag.name, a); config.ngTags.on('changed', tagNameSet => tag.updateNg(tagNameSet)); config.ngLockedTags.on('changed', tagNameSet => tag.updateNgIfLocked(tagNameSet)); return tag; }; } var createTagsBuilder = function(config) { var getTagBy = createTagBuilder(config) return thumbInfoTags => { const tags = thumbInfoTags.map(getTagBy); const ngTagSet = config.ngTags.set; const ngLockedTagSet = config.ngLockedTags.set; for (const t of tags) { t.updateNg(ngTagSet); t.updateNgIfLocked(ngLockedTagSet); } return tags; }; } var createContributorBuilder = function(config) { var typeToMap = Contributor.TYPES.reduce(function(map, type) { return map.set(type, new Map()) }, new Map()) return function(o) { var map = typeToMap.get(o.type) if (map.has(o.id)) return map.get(o.id) var contributor = Contributor.new(o.type, o.id, o.name) map.set(o.id, contributor) contributor.bindToConfig(config) return contributor } } return { forCompleted(movies) { var getTagsBy = createTagsBuilder(movies.config) var getContributorBy = createContributorBuilder(movies.config) return function(thumbInfo) { var m = movies.get(thumbInfo.id) m.description = thumbInfo.description m.tags = getTagsBy(thumbInfo.tags) m.contributor = getContributorBy(thumbInfo.contributor) m.setThumbInfoDone() } }, forErrorOccurred(movies) { return function(thumbInfo) { var m = movies.get(thumbInfo.id) m.error = thumbInfo.error m.setThumbInfoDone() } }, } })() var MovieViewMode = (function(_super) { var MovieViewMode = function(movie, config) { _super.call(this) this.movie = movie this.config = config this.value = this._newViewMode() } MovieViewMode.prototype = createObject(_super.prototype, { _isHiddenByNg() { return !this.config.ngMovieVisible.value && this.movie.ng }, _isHiddenByContributorType() { var c = this.movie.contributor if (c === Contributor.NULL) return false var t = this.config.visibleContributorType.value return !(t === 'all' || t === c.type) }, _isHiddenByVisitedMovieViewMode() { return this.movie.visited && this.config.visitedMovieViewMode.value === 'hide' }, _isHidden() { return this.movie.error.type === 'DELETED' || this._isHiddenByContributorType() || this._isHiddenByNg() || this._isHiddenByVisitedMovieViewMode() }, _isReduced() { return this.movie.visited && this.config.visitedMovieViewMode.value === 'reduce' }, _newViewMode() { if (this._isHidden()) return 'hide' if (this._isReduced()) return 'reduce' return 'doNothing' }, update() { var pre = this.value this.value = this._newViewMode() if (pre !== this.value) this.emit('changed', this.value) }, addListener() { var l = this.update.bind(this) this.movie .on('errorChanged', l) .on('ngChanged', l) .on('visitedChanged', l) .on('contributorChanged', l) ;['ngMovieVisible', 'visibleContributorType', 'visitedMovieViewMode', ].forEach(function(n) { this.config[n].on('changed', l) }, this) return this }, }) return MovieViewMode })(EventEmitter) var MovieViewModes = (function(_super) { var MovieViewModes = function(config) { _super.call(this) this.config = config this._movieToViewMode = new Map() this._emitViewModeChanged = this.emit.bind(this, 'movieViewModeChanged') } MovieViewModes.prototype = createObject(_super.prototype, { get(movie) { var m = this._movieToViewMode if (m.has(movie)) return m.get(movie) var viewMode = new MovieViewMode(movie, this.config) m.set(movie, viewMode) return viewMode.on('changed', this._emitViewModeChanged).addListener() }, sort() { return [...this._movieToViewMode.values()].map(function(m, i) { return {i, m} }).sort(function(a, b) { if (a.m.value === 'hide' && b.m.value !== 'hide') return 1 if (a.m.value !== 'hide' && b.m.value === 'hide') return -1 return a.i - b.i }).map(function(o) { return o.m }) }, }) return MovieViewModes })(EventEmitter) var ConfigDialog = (function(_super) { var isValidStr = function(s) { return typeof s === 'string' && Boolean(s.trim().length) } var isPositiveInt = function(n) { return Number.isSafeInteger(n) && n > 0 } var initCheckbox = function(config, doc, name) { var b = doc.getElementById(name) b.checked = config[name].value b.addEventListener('change', function() { config[name].value = b.checked }) } var optionOf = function(v) { return typeof v === 'object' ? new Option(v.value + ',' + v.text, v.value) : new Option(v, v) } var diffBy = function(target) { var SOMETHING_INPUT_TEXT = '何か入力して下さい。' var POSITIVE_INT_INPUT_TEXT = '1以上の整数を入力して下さい。' var movieUrlOf = function(movieId) { return 'https://www.nicovideo.jp/watch/' + movieId } return { 'ng-movie-id': { targetText: 'NG動画ID', storeName: 'ngMovies', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf: movieUrlOf, }, 'ng-title': { targetText: 'NGタイトル', storeName: 'ngTitles', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf(title) { return 'https://www.nicovideo.jp/search/' + title }, }, 'ng-tag': { targetText: 'NGタグ', storeName: 'ngTags', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf(tag) { return 'https://www.nicovideo.jp/tag/' + tag }, }, 'ng-locked-tag': { targetText: 'NGタグ(ロック)', storeName: 'ngLockedTags', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf(tag) { return 'https://www.nicovideo.jp/tag/' + tag }, }, 'ng-user-id': { targetText: 'NGユーザーID', storeName: 'ngUserIds', convert: Math.trunc, isValid(v) { return isPositiveInt(Math.trunc(v)) }, inputRequestText: POSITIVE_INT_INPUT_TEXT, urlOf(userId) { return 'https://www.nicovideo.jp/user/' + userId }, }, 'ng-user-name': { targetText: 'NGユーザー名', storeName: 'ngUserNames', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf(userName) { return 'https://www.nicovideo.jp/search/' + userName }, }, 'ng-channel-id': { targetText: 'NGチャンネルID', storeName: 'ngChannelIds', convert: Math.trunc, isValid(v) { return isPositiveInt(Math.trunc(v)) }, inputRequestText: POSITIVE_INT_INPUT_TEXT, urlOf(channelId) { return 'https://ch.nicovideo.jp/ch' + channelId }, }, 'visited-movie-id': { targetText: '閲覧済み動画ID', storeName: 'visitedMovies', convert(v) { return v }, isValid: isValidStr, inputRequestText: SOMETHING_INPUT_TEXT, urlOf: movieUrlOf, }, }[target] } var promptFor = async function(target, config, defaultValue) { var d = diffBy(target) var r = '' do { var msg = r ? `"${r}"は登録済みです。\n` : '' r = window.prompt(msg + d.targetText, r || defaultValue || '') if (r === null) return '' while (!d.isValid(r)) { r = window.prompt(d.inputRequestText + '\n' + d.targetText) if (r === null) return '' } } while (!(await config[d.storeName].addAsync(d.convert(r)))) return r } var ConfigDialog = function(config, doc, openInTab) { _super.call(this) this.config = config this.doc = doc this.openInTab = openInTab for (var v of config.ngTitles.array) { this._e('list').add(new Option(v, v)) } this._e('removeAllButton').disabled = !config.ngTitles.array.length initCheckbox(config, doc, 'openNewWindow') initCheckbox(config, doc, 'useGetThumbInfo') initCheckbox(config, doc, 'movieInfoTogglable') initCheckbox(config, doc, 'descriptionTogglable') initCheckbox(config, doc, 'addToNgLockedTags') this._on('target', 'change', this._targetChanged.bind(this)) this._on('addButton', 'click', this._addButtonClicked.bind(this)) this._on('removeButton', 'click', this._removeButtonClicked.bind(this)) this._on('removeAllButton', 'click', this._removeAllButtonClicked.bind(this)) this._on('openButton', 'click', this._openButtonClicked.bind(this)) this._on('closeButton', 'click', this.emit.bind(this, 'closed')) this._on('exportVisibleCheckbox', 'change', this._exportVisibleCheckboxChanged.bind(this)) this._on('importVisibleCheckbox', 'change', this._importVisibleCheckboxChanged.bind(this)) this._on('exportButton', 'click', this._exportButtonClicked.bind(this)) this._on('importButton', 'click', this._importButtonClicked.bind(this)) var updateButtonsDisabled = this._updateButtonsDisabled.bind(this) this._on('target', 'change', updateButtonsDisabled) this._on('list', 'change', updateButtonsDisabled) this._on('addButton', 'click', updateButtonsDisabled) this._on('removeButton', 'click', updateButtonsDisabled) this._on('removeAllButton', 'click', updateButtonsDisabled) } ConfigDialog.prototype = createObject(_super.prototype, { _e(id) { return this.doc.getElementById(id) }, _on(id, eventName, listener) { this._e(id).addEventListener(eventName, listener) }, _diffBySelectedTarget() { return diffBy(this._e('target').value) }, _updateList() { for (var o of Array.from(this._e('list').options)) o.remove() var d = this._diffBySelectedTarget() for (var val of this.config[d.storeName].arrayWithText) { this._e('list').add(optionOf(val)) } }, _targetChanged() { this._updateList() }, _updateButtonsDisabled() { var l = this._e('list') var d = l.selectedIndex === -1 this._e('removeButton').disabled = d this._e('openButton').disabled = d this._e('removeAllButton').disabled = !l.length }, async _addButtonClicked() { var r = await promptFor(this._e('target').value, this.config) if (r) this._e('list').add(new Option(r, r)) }, async _removeButtonClicked() { var opts = Array.from(this._e('list').selectedOptions) var d = this._diffBySelectedTarget() await this.config[d.storeName] .removeAsync(opts.map(function(o) { return d.convert(o.value) })) for (var o of opts) o.remove() }, _removeAllButtonClicked() { var d = this._diffBySelectedTarget() if (!window.confirm(`すべての"${d.targetText}"を削除しますか?`)) return this.config[d.storeName].clear() for (var o of Array.from(this._e('list').options)) o.remove() }, _openButtonClicked() { var opts = Array.from(this._e('list').selectedOptions) var d = this._diffBySelectedTarget() for (var v of opts.map(function(o) { return o.value })) { this.openInTab(d.urlOf(v)) } }, _exportVisibleCheckboxChanged() { var n = this._e('exportVisibleCheckbox').checked ? 'remove' : 'add' this._e('exportContainer').classList[n]('isHidden') }, _importVisibleCheckboxChanged() { var n = this._e('importVisibleCheckbox').checked ? 'remove' : 'add' this._e('importContainer').classList[n]('isHidden') }, async _exportButtonClicked() { var textarea = this._e('exportTextarea') textarea.value = await this.config.toCSV({ ngMovieId: this._e('exportNgMovieIdCheckbox').checked, ngTitle: this._e('exportNgTitleCheckbox').checked, ngTag: this._e('exportNgTagCheckbox').checked, ngUserId: this._e('exportNgUserIdCheckbox').checked, ngUserName: this._e('exportNgUserNameCheckbox').checked, ngChannelId: this._e('exportNgChannelIdCheckbox').checked, visitedMovieId: this._e('exportVisitedMovieIdCheckbox').checked, }) textarea.focus() textarea.select() }, async _importButtonClicked() { await this.config.addFromCSV(this._e('importTextarea').value) this._updateList() this._e('importTextarea').value = '' }, }) ConfigDialog.promptNgTitle = function(config, defaultValue) { promptFor('ng-title', config, defaultValue) } ConfigDialog.promptNgUserName = function(config, defaultValue) { promptFor('ng-user-name', config, defaultValue) } ConfigDialog.SRCDOC = `<!doctype html> <html><head><style> html { margin: 0 auto; max-width: 30em; height: 100%; line-height: 1.5em; } body { height: 100%; margin: 0; display: flex; flex-direction: column; justify-content: center; } .dialog { overflow: auto; padding: 8px; background-color: white; } p { margin: 0; } .listButtonsWrap { display: flex; } .listButtonsWrap .list { flex: auto; } .listButtonsWrap .list select { width: 100%; } .listButtonsWrap .buttons { flex: none; display: flex; flex-direction: column; } .listButtonsWrap .buttons input { margin-bottom: 5px; } .sideComment { margin-left: 2em; } .dialogBottom { text-align: center; } .scriptInfo { text-align: right; } .isHidden { display: none; } textarea { width: 100%; } p:has(#addToNgLockedTags) { display: flex; } </style></head><body> <div class=dialog> <p><select id=target> <option value=ng-movie-id>NG動画ID</option> <option value=ng-title selected>NGタイトル</option> <option value=ng-tag>NGタグ</option> <option value=ng-locked-tag>NGタグ(ロック)</option> <option value=ng-user-id>NGユーザーID</option> <option value=ng-user-name>NGユーザー名</option> <option value=ng-channel-id>NGチャンネルID</option> <option value=visited-movie-id>閲覧済み動画ID</option> </select></p> <div class=listButtonsWrap> <p class=list><select multiple size=10 id=list></select></p> <p class=buttons> <input type=button value=追加 id=addButton> <input type=button value=削除 disabled id=removeButton> <input type=button value=全削除 disabled id=removeAllButton> <input type=button value=開く disabled id=openButton> </p> </div> <p><input type=checkbox id=addToNgLockedTags><label for=addToNgLockedTags>ロックされたタグを[+]ボタンでNG登録するとき、「NGタグ(ロック)」に追加する</label></p> <p><label><input type=checkbox id=openNewWindow>動画を別窓で開く</label></p> <p><label><input type=checkbox id=useGetThumbInfo>動画情報を取得する</label></p> <fieldset id=togglable> <legend>表示・非表示の切り替えボタン</legend> <p><label><input type=checkbox id=movieInfoTogglable>タグ、ユーザー、チャンネル</label></p> <p><label><input type=checkbox id=descriptionTogglable>動画説明</label></p> </fieldset> <p>エクスポート<small><label><input id=exportVisibleCheckbox type=checkbox>表示</label></small></p> <div id=exportContainer class=isHidden> <p><label><input id=exportNgMovieIdCheckbox type=checkbox checked>NG動画ID</label></p> <p><label><input id=exportNgTitleCheckbox type=checkbox checked>NGタイトル</label></p> <p><label><input id=exportNgTagCheckbox type=checkbox checked>NGタグ</label></p> <p><label><input id=exportNgUserIdCheckbox type=checkbox checked>NGユーザーID</label></p> <p><label><input id=exportNgUserNameCheckbox type=checkbox checked>NGユーザー名</label></p> <p><label><input id=exportNgChannelIdCheckbox type=checkbox checked>NGチャンネルID</label></p> <p><label><input id=exportVisitedMovieIdCheckbox type=checkbox checked>閲覧済み動画ID</label></p> <p><input id=exportButton type=button value=エクスポート></p> <p><textarea id=exportTextarea rows=3></textarea></p> </div> <p>インポート<small><label><input id=importVisibleCheckbox type=checkbox>表示</label></small></p> <div id=importContainer class=isHidden> <p><textarea id=importTextarea rows=3></textarea></p> <p><input id=importButton type=button value=インポート></p> </div> <p class=dialogBottom><input type=button value=閉じる id=closeButton></p> <p class=scriptInfo><small><a href=https://greasyfork.org/ja/scripts/880-nico-nico-ranking-ng target=_blank>Nico Nico Ranking NG</a></small></p> </div> </body></html>` return ConfigDialog })(EventEmitter) var NicoPage = (function() { var TOGGLE_OPEN_TEXT = '▼' var TOGGLE_CLOSE_TEXT = '▲' var emphasizeMatchedText = function(e, text, createMatchedElem) { var t = e.textContent if (!text) { e.textContent = t return } var i = t.toUpperCase().indexOf(text) if (i === -1) { e.textContent = t return } while (e.hasChildNodes()) e.removeChild(e.firstChild) var d = e.ownerDocument if (i !== 0) e.appendChild(d.createTextNode(t.slice(0, i))) e.appendChild(createMatchedElem(t.slice(i, i + text.length))) if (i + text.length !== t.length) { e.appendChild(d.createTextNode(t.slice(i + text.length))) } } var MovieTitle = (function() { var MovieTitle = function(elem) { this.elem = elem this._ngTitle = '' this._listeners = new Listeners({ ngIdChanged: set(this, 'ngId'), ngTitleChanged: set(this, 'ngTitle'), }) } MovieTitle.prototype = { get ngId() { return this.elem.classList.contains('nrn-ng-movie-title') }, set ngId(ngId) { var n = ngId ? 'add' : 'remove' this.elem.classList[n]('nrn-ng-movie-title') }, _createNgTitleElem(textContent) { var r###lt = this.elem.ownerDocument.createElement('span') r###lt.className = 'nrn-matched-ng-title' r###lt.textContent = textContent return r###lt }, get ngTitle() { return this._ngTitle }, set ngTitle(ngTitle) { this._ngTitle = ngTitle emphasizeMatchedText(this.elem, ngTitle, this._createNgTitleElem.bind(this)) }, bindToMovie(movie) { this.ngId = movie.ngId this.ngTitle = movie.ngTitle this._listeners.bind(movie) return this }, unbind() { this._listeners.unbind() }, } return MovieTitle })() var ActionPane = (function() { var createVisitButton = function(doc, movie) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-visit-button' r###lt.textContent = '閲覧済み' r###lt.dataset.movieId = movie.id r###lt.dataset.type = 'add' r###lt.dataset.movieTitle = movie.title return r###lt } var createMovieNgButton = function(doc, movie) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-movie-ng-button' r###lt.textContent = 'NG動画' r###lt.dataset.movieId = movie.id r###lt.dataset.type = 'add' r###lt.dataset.movieTitle = movie.title return r###lt } var createTitleNgButton = function(doc, movie) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-title-ng-button' r###lt.textContent = 'NGタイトル追加' r###lt.dataset.movieTitle = movie.title r###lt.dataset.ngTitle = '' return r###lt } var createPane = function(doc) { var r###lt = doc.createElement('div') r###lt.className = 'nrn-action-pane' for (var c of Array.from(arguments).slice(1)) r###lt.appendChild(c) return r###lt } var ActionPane = function(doc, movie) { this.elem = createPane(doc , createVisitButton(doc, movie) , createMovieNgButton(doc, movie) , createTitleNgButton(doc, movie)) this._listeners = new Listeners({ ngIdChanged: set(this, 'ngId'), ngTitleChanged: set(this, 'ngTitle'), visitedChanged: set(this, 'visited'), }) } ActionPane.prototype = { get _visitButton() { return this.elem.querySelector('.nrn-visit-button') }, get visited() { return this._visitButton.dataset.type === 'remove' }, set visited(visited) { var b = this._visitButton b.textContent = visited ? '未閲覧' : '閲覧済み' b.dataset.type = visited ? 'remove' : 'add' }, get _movieNgButton() { return this.elem.querySelector('.nrn-movie-ng-button') }, get ngId() { return this._movieNgButton.dataset.type === 'remove' }, set ngId(ngId) { var b = this._movieNgButton b.textContent = ngId ? 'NG解除' : 'NG登録' b.dataset.type = ngId ? 'remove' : 'add' }, get _titleNgButton() { return this.elem.querySelector('.nrn-title-ng-button') }, get ngTitle() { return this._titleNgButton.dataset.ngTitle }, set ngTitle(ngTitle) { var b = this._titleNgButton b.textContent = ngTitle ? 'NGタイトル削除' : 'NGタイトル追加' b.dataset.type = ngTitle ? 'remove' : 'add' b.dataset.ngTitle = ngTitle }, bindToMovie(movie) { this.ngId = movie.ngId this.ngTitle = movie.ngTitle this.visited = movie.visited this._listeners.bind(movie) return this }, unbind() { this._listeners.unbind() }, } return ActionPane })() var TagView = (function() { var createElem = function(doc, tag) { var a = doc.createElement('a') a.className = 'nrn-movie-tag-link' a.target = '_blank' a.textContent = tag.name a.href = 'https://www.nicovideo.jp/tag/' + tag.name const key = doc.createElement('span'); key.textContent = tag.lock ? '🔒' : ''; var b = doc.createElement('span') b.className = 'nrn-tag-ng-button' b.textContent = '[+]' b.dataset.type = 'add' b.dataset.tagName = tag.name if (tag.lock) b.dataset.lock = 'true'; var r###lt = doc.createElement('span') r###lt.className = 'nrn-movie-tag' r###lt.appendChild(a) r###lt.appendChild(key); r###lt.appendChild(b) return r###lt } var TagView = function(doc, tag) { this.tagName = tag.name; this.elem = createElem(doc, tag); this._listeners = new Listeners({ngChanged: set(this, 'ng')}) } TagView.prototype = { get _link() { return this.elem.querySelector('.nrn-movie-tag-link') }, get ng() { return this._link.classList.contains('nrn-movie-ng-tag-link') }, set ng(ng) { this._link.classList[ng ? 'add' : 'remove']('nrn-movie-ng-tag-link') var b = this.elem.querySelector('.nrn-tag-ng-button') b.textContent = ng ? '[x]' : '[+]' b.dataset.type = ng ? 'remove' : 'add' }, bindToTag(tag) { this.ng = tag.ng this._listeners.bind(tag) return this }, unbind() { this._listeners.unbind() }, } return TagView })() var ContributorView = (function() { var ContributorView = function(doc, contributor) { this.contributor = contributor this.elem = this._createElem(doc) } ContributorView.prototype = { _createElem(doc) { var a = doc.createElement('a') a.className = 'nrn-contributor-link' a.target = '_blank' a.href = this.contributor.url a.textContent = this.contributor.name || '(名前不明)' var b = doc.createElement('span') this._setNgButton(b) var r###lt = doc.createElement('span') r###lt.className = 'nrn-contributor' r###lt.appendChild(doc.createTextNode(this._label)) r###lt.appendChild(a) r###lt.appendChild(b) return r###lt }, _initContributorDataset(dataset) { dataset.contributorType = this.contributor.type dataset.id = this.contributor.id dataset.name = this.contributor.name dataset.type = 'add' }, get _label() { throw new Error('must be implemented') }, _setNgButton() { throw new Error('must be implemented') }, _bindToContributor() { throw new Error('must be implemented') }, } var UserView = function UserView(doc, contributor) { ContributorView.call(this, doc, contributor) this._listeners = new Listeners({ ngIdChanged: set(this, 'ngId'), ngNameChanged: set(this, 'ngName'), }) this._bindToContributor() } UserView.prototype = createObject(ContributorView.prototype, { get _label() { return 'ユーザー: ' }, _setNgButton(b) { var d = b.ownerDocument var ngIdButton = d.createElement('span') ngIdButton.className = 'nrn-contributor-ng-id-button' ngIdButton.textContent = '+ID' this._initContributorDataset(ngIdButton.dataset) var ngNameButton = d.createElement('span') ngNameButton.className = 'nrn-contributor-ng-name-button' ngNameButton.textContent = '+名' this._initContributorDataset(ngNameButton.dataset) b.className = 'nrn-user-ng-button' b.appendChild(d.createTextNode('[')) b.appendChild(ngIdButton) if (this.contributor.name) { b.appendChild(d.createTextNode('/')) } else { ngNameButton.style.display = 'none' } b.appendChild(ngNameButton) b.appendChild(d.createTextNode(']')) }, get ngId() { return this.elem.querySelector('.nrn-contributor-link') .classList.contains('nrn-ng-id-contributor-link') }, set ngId(ngId) { var a = this.elem.querySelector('.nrn-contributor-link') a.classList[ngId ? 'add' : 'remove']('nrn-ng-id-contributor-link') var b = this.elem.querySelector('.nrn-contributor-ng-id-button') b.textContent = ngId ? 'xID' : '+ID' b.dataset.type = ngId ? 'remove' : 'add' }, get ngName() { var e = this.elem.querySelector('.nrn-matched-ng-contributor-name') return e ? e.textContent : '' }, set ngName(ngName) { var b = this.elem.querySelector('.nrn-contributor-ng-name-button') b.textContent = ngName ? 'x名' : '+名' b.dataset.type = ngName ? 'remove' : 'add' b.dataset.matched = ngName emphasizeMatchedText( this.elem.querySelector('.nrn-contributor-link'), ngName, function(text) { var r###lt = this.elem.ownerDocument.createElement('span') r###lt.className = 'nrn-matched-ng-contributor-name' r###lt.textContent = text return r###lt }.bind(this)) }, _bindToContributor() { this.ngId = this.contributor.ngId this.ngName = this.contributor.ngName this._listeners.bind(this.contributor) return this }, unbind() { this._listeners.unbind() }, }) var ChannelView = function ChannelView(doc, contributor) { ContributorView.call(this, doc, contributor) this._listeners = new Listeners({ngChanged: set(this, 'ng')}) this._bindToContributor() } ChannelView.prototype = createObject(ContributorView.prototype, { get _label() { return 'チャンネル: ' }, _setNgButton(e) { e.className = 'nrn-contributor-ng-button' e.textContent = '[+]' this._initContributorDataset(e.dataset) }, get ng() { return this.elem.querySelector('.nrn-contributor-link') .classList.contains('nrn-ng-contributor-link') }, set ng(ng) { var a = this.elem.querySelector('.nrn-contributor-link') a.classList[ng ? 'add' : 'remove']('nrn-ng-contributor-link') var b = this.elem.querySelector('.nrn-contributor-ng-button') b.textContent = ng ? '[x]' : '[+]' b.dataset.type = ng ? 'remove' : 'add' }, _bindToContributor() { this.ng = this.contributor.ng this._listeners.bind(this.contributor) return this }, unbind() { this._listeners.unbind() }, }) ContributorView.new = function(doc, contributor) { switch (contributor.type) { case 'user': return new UserView(doc, contributor) case 'channel': return new ChannelView(doc, contributor) default: throw new Error(contributor.type) } } return ContributorView })() var MovieInfo = (function() { var createElem = function(doc) { var e = doc.createElement('P') e.className = 'nrn-error' var t = doc.createElement('p') t.className = 'nrn-tag-container' var c = doc.createElement('p') c.className = 'nrn-contributor-container' var r###lt = doc.createElement('div') r###lt.className = 'nrn-movie-info-container' r###lt.appendChild(e) r###lt.appendChild(t) r###lt.appendChild(c) return r###lt } var createToggle = function(doc) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-movie-info-toggle' r###lt.textContent = TOGGLE_OPEN_TEXT return r###lt } var MovieInfo = function(doc) { this.elem = createElem(doc) this.toggle = createToggle(doc) this.togglable = true this._tagViews = [] this._contributorView = null this._error = Movie.NO_ERROR this._actionPane = null this._listeners = new Listeners({ tagsChanged: this._createAndSetTagViews.bind(this), contributorChanged: this._createAndSetContributorView.bind(this), errorChanged: set(this, 'error'), }) } MovieInfo.prototype = { set actionPane(actionPane) { this._actionPane = actionPane this.elem.insertBefore(actionPane.elem, this.elem.firstChild) }, get tagViews() { return this._tagViews }, set tagViews(tagViews) { this._tagViews = tagViews var e = this.elem.querySelector('.nrn-tag-container') for (var v of tagViews) e.appendChild(v.elem) }, get contributorView() { return this._contributorView }, set contributorView(contributorView) { this._contributorView = contributorView this.elem.querySelector('.nrn-contributor-container') .appendChild(contributorView.elem) }, get error() { return this._error }, set error(error) { if (this._error === error) return this._error = error this.elem.querySelector('.nrn-error').textContent = error.message }, hasAny() { return Boolean(this.elem.querySelector('.nrn-action-pane') || this.elem.querySelector('.nrn-movie-tag') || this.elem.querySelector('.nrn-contributor') || this.error !== Movie.NO_ERROR) }, _createAndSetTagViews(tags) { var d = this.elem.ownerDocument this.tagViews = tags.map(function(tag) { return new TagView(d, tag).bindToTag(tag) }) }, _createAndSetContributorView(contributor) { if (contributor === Contributor.NULL) return var d = this.elem.ownerDocument this.contributorView = ContributorView.new(d, contributor) }, bindToMovie(movie) { this._createAndSetTagViews(movie.tags) this._createAndSetContributorView(movie.contributor) this.error = movie.error if (!movie.thumbInfoDone) this._listeners.bind(movie) }, unbind() { this._listeners.unbind() this.tagViews.forEach(function(v) { v.unbind() }) if (this.contributorView) this.contributorView.unbind() if (this._actionPane) this._actionPane.unbind() }, } return MovieInfo })() var Description = (function() { var re = /(sm|so|nm|co|ar|im|lv|mylist\/|watch\/|user\/)(?:\d+)/g var typeToHRef = { sm: 'https://www.nicovideo.jp/watch/', so: 'https://www.nicovideo.jp/watch/', nm: 'https://www.nicovideo.jp/watch/', co: 'https://com.nicovideo.jp/community/', ar: 'https://ch.nicovideo.jp/article/', im: 'https://seiga.nicovideo.jp/seiga/', lv: 'http://live.nicovideo.jp/watch/', 'mylist/': 'https://www.nicovideo.jp/', 'watch/': 'https://www.nicovideo.jp/', 'user/': 'https://www.nicovideo.jp/', } var createAnchor = function(doc, href, text) { var a = doc.createElement('a') a.target = '_blank' a.href = href a.textContent = text return a } var createCloseButton = function(doc) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-description-close-button' r###lt.textContent = TOGGLE_CLOSE_TEXT return r###lt } var createElem = function(doc, closeButton) { var text = doc.createElement('span') text.className = 'nrn-description-text' var r###lt = doc.createElement('p') r###lt.className = 'itemDescription ranking nrn-description' r###lt.appendChild(text) r###lt.appendChild(closeButton) return r###lt } var createOpenButton = function(doc) { var r###lt = doc.createElement('span') r###lt.className = 'nrn-description-open-button' r###lt.textContent = TOGGLE_OPEN_TEXT return r###lt } var Description = function(doc) { this.closeButton = createCloseButton(doc) this.elem = createElem(doc, this.closeButton) this.openButton = createOpenButton(doc) this.original = null this.text = '' this.linkified = false this.togglable = true this._listeners = new Listeners({ 'descriptionChanged': set(this, 'text'), }) } Description.prototype = { linkify() { if (this.linkified) return this.linkified = true var t = this.text var d = this.elem.ownerDocument var f = d.createDocumentFragment() var lastIndex = 0 for (var r; r = re.exec(t);) { f.appendChild(d.createTextNode(t.slice(lastIndex, r.index))) f.appendChild(createAnchor(d, typeToHRef[r[1]] + r[0], r[0])) lastIndex = re.lastIndex } f.appendChild(d.createTextNode(t.slice(lastIndex))) f.normalize() this.elem.firstChild.appendChild(f) }, bindToMovie(movie) { this.text = movie.description this._listeners.bind(movie) }, unbind() { this._listeners.unbind() }, } return Description })() var MovieRoot = (function() { var MovieRoot = function(elem) { this.elem = elem var d = elem.ownerDocument this.movieInfo = new MovieInfo(d) this.description = new Description(d) this._openNewWindow = false this.movieTitle = null this._movieListeners = new Listeners({ thumbInfoDone: this.setThumbInfoDone.bind(this), }) this._movieViewModeListeners = new Listeners({ changed: set(this, 'viewMode'), }) this._configOpenNewWindowListeners = new Listeners({ changed: set(this, 'openNewWindow'), }) } MovieRoot.prototype = { markMovieAnchor() { for (var a of this._movieAnchors) a.dataset.nrnMovieAnchor = 'true' }, set id(id) { for (var a of this._movieAnchors) a.dataset.nrnMovieId = id }, get titleElem() { throw new Error('must be implemented') }, set title(title) { this.titleElem.textContent = title for (var a of this._movieAnchors) a.dataset.nrnMovieTitle = title }, get _reduced() { return this.elem.classList.contains('nrn-reduce') }, _halfThumb() {}, _restoreThumb() {}, _reduce() { this.elem.classList.add('nrn-reduce') this._halfThumb() }, _unreduce() { this.elem.classList.remove('nrn-reduce') this._restoreThumb() }, get _hidden() { return this.elem.classList.contains('nrn-hide') }, _hide() { this.elem.classList.add('nrn-hide') }, _show() { this.elem.classList.remove('nrn-hide') }, get viewMode() { if (this.elem.classList.contains('nrn-reduce')) return 'reduce' if (this.elem.classList.contains('nrn-hide')) return 'hide' return 'doNothing' }, set viewMode(viewMode) { if (this._reduced) this._unreduce() else if (this._hidden) this._show() switch (viewMode) { case 'reduce': this._reduce(); break case 'hide': this._hide(); break case 'doNothing': break default: throw new Error(viewMode) } }, get _movieAnchorSelectors() { throw new Error('must be implemented') }, get _movieAnchors() { var r###lt = [] for (var s of this._movieAnchorSelectors) { var a = this.elem.querySelector(s) if (a) r###lt.push(a) } return r###lt }, get openNewWindow() { return this._openNewWindow }, set openNewWindow(openNewWindow) { this._openNewWindow = openNewWindow var t = openNewWindow ? '_blank' : '' for (var a of this._movieAnchors) a.target = t }, get _movieInfoVisible() { return Boolean(this.movieInfo.elem.parentNode) }, set _movieInfoVisible(visible) { if (visible) { this._addMovieInfo() this.movieInfo.toggle.textContent = TOGGLE_CLOSE_TEXT } else { this.movieInfo.elem.remove() this.movieInfo.toggle.textContent = TOGGLE_OPEN_TEXT } }, toggleMovieInfo() { this._movieInfoVisible = !this._movieInfoVisible }, set actionPane(actionPane) { this.movieInfo.actionPane = actionPane }, _addMovieInfo() { throw new Error('must be implemented') }, _addMovieInfoToggle() { this.elem.querySelector('.itemData') .appendChild(this.movieInfo.toggle) }, setMovieInfoToggleIfRequired() {}, _updateByMovieInfoTogglable() { if (!this.movieInfo.hasAny()) return if (this.movieInfo.togglable) { this._addMovieInfoToggle() } else { this.movieInfo.toggle.remove() } this._movieInfoVisible = !this.movieInfo.togglable }, get movieInfoTogglable() { return this.movieInfo.togglable }, set movieInfoTogglable(movieInfoTogglable) { this.movieInfo.togglable = movieInfoTogglable this._updateByMovieInfoTogglable() }, _queryOriginalDescriptionElem() { return this.elem.querySelector('.itemDescription') }, get _originalDescriptionElem() { var r###lt = this.description.original if (!r###lt) { r###lt = this.description.original = this._queryOriginalDescriptionElem() } return r###lt }, get _descriptionExpanded() { return Boolean(this.description.elem.parentNode) }, set _descriptionExpanded(expanded) { var o = this._originalDescriptionElem var d = this.description if (expanded && o.parentNode) { d.linkify() o.parentNode.replaceChild(d.elem, o) } else if (!expanded && d.elem.parentNode) { d.elem.parentNode.replaceChild(o, d.elem) } }, _updateByDescriptionTogglable() { if (!this.description.text) return if (this.description.togglable) { this._originalDescriptionElem?.appendChild(this.description.openButton) this.description.elem.appendChild(this.description.closeButton) } else { this.description.closeButton.remove() } this._descriptionExpanded = !this.description.togglable }, toggleDescription() { this._descriptionExpanded = !this._descriptionExpanded }, get descriptionTogglable() { return this.description.togglable }, set descriptionTogglable(descriptionTogglable) { this.description.togglable = descriptionTogglable this._updateByDescriptionTogglable() }, setThumbInfoDone() { this.elem.classList.add('nrn-thumb-info-done') }, get thumbInfoDone() { return this.elem.classList.contains('nrn-thumb-info-done') }, bindToMovie(movie) { this.movieInfo.bindToMovie(movie) this.description.bindToMovie(movie) if (movie.thumbInfoDone) this.setThumbInfoDone() else this._movieListeners.bind(movie) }, bindToMovieViewMode(movieViewMode) { this.viewMode = movieViewMode.value this._movieViewModeListeners.bind(movieViewMode) }, bindToConfig(config) { this.openNewWindow = config.openNewWindow.value this._configOpenNewWindowListeners.bind(config.openNewWindow) }, unbind() { this.movieInfo.unbind() this.description.unbind() this._movieListeners.unbind() this._movieViewModeListeners.unbind() this._configOpenNewWindowListeners.unbind() if (this.movieTitle) this.movieTitle.unbind() }, } return MovieRoot })() var ConfigBar = (function() { var createConfigBar = function(doc) { var html = `<div id=nrn-config-bar> <label> 閲覧済みの動画を <select id=nrn-visited-movie-view-mode-select> <option value=reduce>縮小</option> <option value=hide>非表示</option> <option value=doNothing>通常表示</option> </select> </label> | <label> 投稿者 <select id=nrn-visible-contributor-type-select> <option value=all>全部</option> <option value=user>ユーザー</option> <option value=channel>チャンネル</option> </select> </label> | <label><input type=checkbox id=nrn-ng-movie-visible-checkbox> NG動画を表示</label> | <span id=nrn-config-button>設定</span> </div>` var e = doc.createElement('div') e.innerHTML = html var r###lt = e.firstChild r###lt.remove() return r###lt } var ConfigBar = function(doc) { this.elem = createConfigBar(doc) } ConfigBar.prototype = { get _viewModeSelect() { return this.elem.querySelector('#nrn-visited-movie-view-mode-select') }, get visitedMovieViewMode() { return this._viewModeSelect.value }, set visitedMovieViewMode(viewMode) { this._viewModeSelect.value = viewMode }, get _visibleContributorTypeSelect() { return this.elem.querySelector('#nrn-visible-contributor-type-select') }, get visibleContributorType() { return this._visibleContributorTypeSelect.value }, set visibleContributorType(type) { this._visibleContributorTypeSelect.value = type }, bindToConfig(config) { this.visitedMovieViewMode = config.visitedMovieViewMode.value this.visibleContributorType = config.visibleContributorType.value config.visitedMovieViewMode.on('changed', set(this, 'visitedMovieViewMode')) config.visibleContributorType.on('changed', set(this, 'visibleContributorType')) return this }, } return ConfigBar })() var NicoPage = function(doc) { this.doc = doc this._toggleToMovieRoot = new Map() } NicoPage.prototype = { createConfigBar() { return new ConfigBar(this.doc) }, createTables() { return [] }, createMovieRoot() { throw new Error('must be implemented') }, get _configBarContainer() { throw new Error('must be implemented') }, addConfigBar(bar) { var target = this._configBarContainer target.insertBefore(bar.elem, target.firstChild) }, parse() { throw new Error('must be implemented') }, mapToggleTo(movieRoot) { var m = this._toggleToMovieRoot m.set(movieRoot.movieInfo.toggle, movieRoot) m.set(movieRoot.description.openButton, movieRoot) m.set(movieRoot.description.closeButton, movieRoot) }, unmapToggleFrom(movieRoot) { var m = this._toggleToMovieRoot m.delete(movieRoot.movieInfo.toggle) m.delete(movieRoot.description.openButton) m.delete(movieRoot.description.closeButton) }, getMovieRootBy(toggle) { return this._toggleToMovieRoot.get(toggle) }, _configDialogLoaded() {}, showConfigDialog(config) { var back = this.doc.createElement('div') back.style.backgroundColor = 'black' back.style.opacity = '0.5' back.style.zIndex = '10000' back.style.position = 'fixed' back.style.top = '0' back.style.left = '0' back.style.width = '100%' back.style.height = '100%' this.doc.body.appendChild(back) var f = this.doc.createElement('iframe') f.style.position = 'fixed' f.style.top = '0' f.style.left = '0' f.style.width = '100%' f.style.height = '100%' f.style.zIndex = '10001' f.srcdoc = ConfigDialog.SRCDOC f.addEventListener('load', function loaded() { this._configDialogLoaded(f.contentDocument) const openInTab = typeof GM_openInTab === 'undefined' ? GM.openInTab : GM_openInTab new ConfigDialog(config, f.contentDocument, openInTab) .on('closed', function() { f.remove() back.remove() }) }.bind(this)) this.doc.body.appendChild(f) }, bindToConfig() {}, get _pendingMoviesInvisibleCss() { throw new Error('must be implemented') }, _createPendingMoviesInvisibleStyle() { var r###lt = this.doc.createElement('style') r###lt.id = 'nrn-pending-movies-hide-style' r###lt.textContent = this._pendingMoviesInvisibleCss return r###lt }, set pendingMoviesVisible(v) { var id = 'nrn-pending-movies-hide-style' if (v) { this.doc.getElementById(id).remove() } else { if (!this.doc.head) { new MutationObserver((recs, observer) => { if (!this.doc.head) return; this.doc.head.appendChild(this._createPendingMoviesInvisibleStyle()); observer.disconnect(); }).observe(this.doc, {childList: true, subtree: true}); } else { this.doc.head.appendChild(this._createPendingMoviesInvisibleStyle()); } } }, get css() { throw new Error('must be implemented') }, observeMutation() {}, } Object.assign(NicoPage, { MovieTitle, ActionPane, TagView, ContributorView, MovieInfo, Description, MovieRoot, ConfigBar, }) return NicoPage })() var ListPage = (function(_super) { var MovieRoot = (function(_super) { var MovieRoot = function(elem) { _super.call(this, elem) } MovieRoot.prototype = createObject(_super.prototype, { get titleElem() { return this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObjectTitle.NC-VideoMediaObject-title') }, get _movieAnchorSelectors() { return ['.NC-MediaObject.NC-VideoMediaObject .NC-Link.NC-MediaObject-contents'] }, set actionPane(actionPane) { this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObject-main')?.appendChild(actionPane.elem) }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setMovieInfoToggleIfRequired() { if (!this.movieInfo.toggle.parentNode) { this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObject-action')?.appendChild(this.movieInfo.toggle) } }, }) return MovieRoot })(_super.MovieRoot) var SubMovieRoot = (function(_super) { var SubMovieRoot = function(elem) { _super.call(this, elem) elem.classList.add('nrn-sub-movie-root') } SubMovieRoot.prototype = createObject(_super.prototype, { get titleElem() { return this.elem.querySelector('.RankingSubVideo-title') }, get _movieAnchorSelectors() { return ['.RankingSubVideo-title', '.RankingSubVideo-thumbnail'] }, set actionPane(actionPane) { this.movieInfo.actionPane = actionPane }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setMovieInfoToggleIfRequired() { if (!this.movieInfo.toggle.parentNode) { this.elem.appendChild(this.movieInfo.toggle) } }, }) return SubMovieRoot })(_super.MovieRoot) var AdMovieRoot = (function(_super) { var AdMovieRoot = function(elem) { _super.call(this, elem) elem.classList.add('nrn-ad-movie-root') } AdMovieRoot.prototype = createObject(_super.prototype, { get titleElem() { return this.elem.querySelector('.NC-MediaObjectTitle.RankingMainNicoad') }, get _movieAnchorSelectors() { return ['.NC-Link.NC-MediaObject-contents'] }, set actionPane(actionPane) { this.elem.querySelector('.NC-MediaObject-main').appendChild(actionPane.elem) }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setMovieInfoToggleIfRequired() { if (!this.movieInfo.toggle.parentNode) { this.elem.querySelector('.NC-MediaObject-action').appendChild(this.movieInfo.toggle) } }, }) return AdMovieRoot })(_super.MovieRoot) var parent = function(className, child) { for (var e = child; e; e = e.parentNode) { if (e.classList.contains(className)) return e } return null } var ListPage = function(doc) { _super.call(this, doc) } ListPage.prototype = createObject(_super.prototype, { createTables() { return [] }, createMovieRoot(r###ltOfParsing) { switch (r###ltOfParsing.type) { case 'main': return new MovieRoot(r###ltOfParsing.rootElem) case 'sub': return new SubMovieRoot(r###ltOfParsing.rootElem) case 'ad': return new AdMovieRoot(r###ltOfParsing.rootElem) default: throw new Error(r###ltOfParsing.type) } }, get _configBarContainer() { return this.doc.querySelector('.RankingVideoListContainer') }, parse(target) { target = target || this.doc return this._parseMain(target).concat(this._pars###b(target)) }, _parseMain(target) { return Array.from(target.querySelectorAll('.NC-VideoMediaObjectWrapper')) .map(function(item) { const ncLink = item.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-Link'); const id = ncLink ? movieIdOf(ncLink.href) : null; return { type: 'main', movie: { id, title: item.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObjectTitle.NC-VideoMediaObject-title')?.textContent?.trim() ?? "", }, rootElem: item, } }).filter(e => Boolean(e.movie.id)); }, _pars###b(target) { var selector = '.HotTopicVideosContainer > .BaseRankingContentContainer-main > .RankingSubVideo' + ', .SpecifiedGenreRankingVideosContainer-subVideos > .RankingSubVideo' return Array.from(target.querySelectorAll(selector)) .map(function(rootElem) { return { type: 'sub', movie: { id: rootElem.querySelector('.RankingSubVideo-thumbnail').pathname.slice('/watch/'.length), title: rootElem.querySelector('.Thumbnail.RankingSubVideo .Thumbnail-image').getAttribute('alt').trim(), }, rootElem, } }) }, _configDialogLoaded(doc) { doc.getElementById('togglable').hidden = true }, observeMutation(callback) { const nodeList = document.querySelectorAll('.NC-MediaObject.NC-NicoadMediaObject.RankingMainNicoad') for (const node of Array.from(nodeList)) { new MutationObserver((records, observer) => { const {target} = records[0] if (!target.dataset.contentId) return const titleElem = target.querySelector('.NC-MediaObjectTitle.RankingMainNicoad') if (!titleElem) return observer.disconnect() const id = target.dataset.contentId const title = titleElem.textContent.trim() callback([{ type: 'ad', movie: {id, title}, rootElem: target.parentNode, }], true) }).observe(node, { attributes: true, attributeFilter: ['data-content-id'], }) } }, get _pendingMoviesInvisibleCss() { return `.NC-VideoMediaObjectWrapper, .MediaObject.RankingSubVideo, .NC-NicoadMediaObjectWrapper { visibility: hidden; } .NC-VideoMediaObjectWrapper[data-sensitive], .NC-VideoMediaObjectWrapper.nrn-thumb-info-done, .MediaObject.RankingSubVideo.nrn-thumb-info-done, .NC-NicoadMediaObjectWrapper.nrn-thumb-info-done, .NC-NicoadMediaObjectWrapper[data-blank] { visibility: inherit; } ` }, get css() { return `#nrn-config-button, .nrn-visit-button:hover, .nrn-movie-ng-button:hover, .nrn-title-ng-button:hover, .nrn-tag-ng-button:hover, .nrn-contributor-ng-button:hover, .nrn-contributor-ng-id-button:hover, .nrn-contributor-ng-name-button:hover, .nrn-movie-info-toggle:hover { text-decoration: underline; cursor: pointer; } .nrn-movie-tag { display: inline-block; margin-right: 1em; } .nrn-movie-tag-link, .nrn-contributor-link { color: #333333; } .nrn-movie-tag-link.nrn-movie-ng-tag-link, .nrn-contributor-link.nrn-ng-contributor-link, .nrn-matched-ng-contributor-name, .nrn-matched-ng-title { color: white; background-color: fuchsia; } .NC-MediaObject.RankingMainNicoad.nrn-thumb-info-done.nrn-reduce .RankingMainNicoad-meta { border-top: 0px; } .nrn-movie-info-container .nrn-tag-container, .nrn-movie-info-container .nrn-contributor-container { line-height: 1.5em; margin-top: 4px; } .nrn-hide, .NC-VideoMediaObjectWrapper.nrn-reduce .RankingMainVideo .RankingMainVideo-description, .RankingMainNicoad.nrn-reduce .RankingMainNicoad-message div[data-nicoad-message], .BaseRankingContentContainer.SpecifiedGenreRankingVideosContainer .SpecifiedGenreRankingVideosContainer-subVideo.nrn-hide { display: none; } .NC-VideoMediaObjectWrapper.nrn-reduce .RankingMainVideo .NC-VideoMediaObject-thumbnail, .NC-NicoadMediaObjectWrapper.nrn-reduce .RankingMainNicoad .NC-NicoadMediaObject-thumbnail { width: 130px; } .nrn-user-ng-button { display: inline-block; } .NC-MediaObject.RankingMainVideo .NC-VideoMediaObject-title.nrn-ng-movie-title, .nrn-contributor-link.nrn-ng-id-contributor-link, .MediaObject.RankingSubVideo .RankingSubVideo-title.nrn-ng-movie-title, .NC-MediaObject.RankingMainNicoad .NC-MediaObjectTitle.nrn-ng-movie-title { text-decoration: line-through; } .RankingMainNicoad { position: relative; } .RankingMainVideo .nrn-action-pane, .RankingMainNicoad .nrn-action-pane { display: none; position: absolute; top: 0px; right: 35px; padding: 3px; color: #999; background-color: rgb(105, 105, 105); z-index: 11; } .RankingMainVideo:hover .nrn-action-pane, .RankingMainNicoad:hover .nrn-action-pane { display: block; } .RankingMainVideo:hover .nrn-action-pane .nrn-visit-button, .RankingMainVideo:hover .nrn-action-pane .nrn-movie-ng-button, .RankingMainVideo:hover .nrn-action-pane .nrn-title-ng-button, .RankingMainNicoad:hover .nrn-action-pane .nrn-visit-button, .RankingMainNicoad:hover .nrn-action-pane .nrn-movie-ng-button, .RankingMainNicoad:hover .nrn-action-pane .nrn-title-ng-button { color: white; } .RankingMainVideo:hover .nrn-action-pane .nrn-movie-ng-button, .RankingMainVideo:hover .nrn-action-pane .nrn-title-ng-button, .RankingMainNicoad:hover .nrn-action-pane .nrn-movie-ng-button, .RankingMainNicoad:hover .nrn-action-pane .nrn-title-ng-button { margin-left: 5px; border-left: solid thin; padding-left: 5px; } .NC-MediaObject.RankingMainNicoad.nrn-thumb-info-done .RankingMainNicoad-point { right: 2em; } .RankingMainVideo .nrn-movie-info-toggle, .RankingMainNicoad .nrn-movie-info-toggle { display: block; color: #999; text-align: center; } .nrn-sub-movie-root { position: relative; } .nrn-sub-movie-root .nrn-movie-info-toggle { display: block; text-align: right; background-color: white; } .nrn-sub-movie-root .nrn-movie-info-container { display: block; position: static; padding-top: 5px; } .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button, .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button, .nrn-sub-movie-root .nrn-action-pane .nrn-title-ng-button { display: inline-block; color: #333333; } .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button, .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button { margin-right: 0.5em; } .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-toggle { position: absolute; right: 0px; bottom: 0px; padding: 1em 0 0 1em; background: rgba(255, 255, 255, 0); } .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-container { display: none; position: absolute; top: 60px; background-color: lightgray; z-index: 100; padding-left: 5px; } .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root:hover .nrn-movie-info-container, .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-container:hover { display: block; } .nrn-error { color: red; } ` }, }) Object.assign(ListPage, { MovieRoot, SubMovieRoot, }) return ListPage })(NicoPage) var SearchPage = (function(_super) { var AbstractMovieRoot = (function(_super) { var AbstractMovieRoot = function(elem) { _super.call(this, elem) } AbstractMovieRoot.prototype = createObject(_super.prototype, { get titleElem() { return this.elem.querySelector('.itemTitle a') }, get _movieAnchorSelectors() { return ['.itemTitle a', '.itemThumbWrap'] }, }) return AbstractMovieRoot })(_super.MovieRoot) var FixedThumbMovieRoot = (function(_super) { var FixedThumbMovieRoot = function(elem) { _super.call(this, elem) } FixedThumbMovieRoot.prototype = createObject(_super.prototype, { _getThumbElement() { const e = this.elem.querySelector('.thumb') return e ? e : this.elem.querySelector('.backgroundThumbnail') }, _halfThumb() { var e = this._getThumbElement() if (!e) return var s = e.style if (!s.marginTop) return s.marginTop = '-9px' s.width = '80px' s.height = '63px' }, _restoreThumb() { var e = this._getThumbElement() if (!e) return var s = e.style if (!s.marginTop) return s.marginTop = '-15px' s.width = '160px' s.height = '' }, }) return FixedThumbMovieRoot })(AbstractMovieRoot) var TwoColumnMovieRoot = (function(_super) { var TwoColumnMovieRoot = function(elem) { _super.call(this, elem) } TwoColumnMovieRoot.prototype = createObject(_super.prototype, { set actionPane(actionPane) { this.elem.appendChild(actionPane.elem) }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setThumbInfoDone() { _super.prototype.setThumbInfoDone.call(this) this._updateByMovieInfoTogglable() this._updateByDescriptionTogglable() }, }) return TwoColumnMovieRoot })(FixedThumbMovieRoot) var FourColumnMovieRoot = (function(_super) { var FourColumnMovieRoot = function(elem) { _super.call(this, elem) elem.classList.add('nrn-4-column-item') } FourColumnMovieRoot.prototype = createObject(_super.prototype, { set actionPane(actionPane) { this.movieInfo.actionPane = actionPane }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setMovieInfoToggleIfRequired() { if (!this.movieInfo.toggle.parentNode) { this.elem.appendChild(this.movieInfo.toggle) } }, }) return FourColumnMovieRoot })(FixedThumbMovieRoot) var MovieRoot = (function(_super) { var MovieRoot = function(elem) { _super.call(this, elem) } MovieRoot.prototype = createObject(_super.prototype, { set actionPane(actionPane) { this.elem.appendChild(actionPane.elem) }, _addMovieInfo() { this.elem.querySelector('.itemContent') .appendChild(this.movieInfo.elem) }, setThumbInfoDone() { _super.prototype.setThumbInfoDone.call(this) this._updateByMovieInfoTogglable() this._updateByDescriptionTogglable() }, bindToConfig(config) { _super.prototype.bindToConfig.call(this, config) this.movieInfoTogglable = config.movieInfoTogglable.value config.movieInfoTogglable.on('changed', set(this, 'movieInfoTogglable')) this.descriptionTogglable = config.descriptionTogglable.value config.descriptionTogglable.on('changed', set(this, 'descriptionTogglable')) }, }) return MovieRoot })(FixedThumbMovieRoot) var SubMovieRoot = (function(_super) { var SubMovieRoot = function(elem) { _super.call(this, elem) elem.classList.add('nrn-sub-movie-root') } SubMovieRoot.prototype = createObject(_super.prototype, { set actionPane(actionPane) { this.movieInfo.actionPane = actionPane }, _addMovieInfo() { this.elem.appendChild(this.movieInfo.elem) }, setMovieInfoToggleIfRequired() { if (!this.movieInfo.toggle.parentNode) { this.elem.appendChild(this.movieInfo.toggle) } }, }) return SubMovieRoot })(AbstractMovieRoot) var createMainMovieRoot = function(rootElem) { var singleColumnView = Boolean(rootElem.getElementsByClassName('videoList01Wrap').length) if (singleColumnView) return new MovieRoot(rootElem) var twoColumnView = Boolean(rootElem.getElementsByClassName('videoList02Wrap').length) if (twoColumnView) return new TwoColumnMovieRoot(rootElem) return new FourColumnMovieRoot(rootElem) } var SearchPage = function(doc) { _super.call(this, doc) } SearchPage.prototype = createObject(_super.prototype, { removeEmbeddedStyle() { const nodeList = document.querySelectorAll('.itemContent[style="visibility: visible;"]'); for (const node of Array.from(nodeList)) { node.style.visibility = ''; } }, parse(target) { target = target || this.doc return this._parseMain(target).concat(this._pars###b(target)) }, _parseItem(item) { return { type: 'main', movie: { id: item.dataset.videoId, title: item.querySelector('.itemTitle a').title, }, rootElem: item, } }, parseAutoPagerizedNodes(target) { return [this._parseItem(target)] }, _parseMain(target) { return Array.from(target.querySelectorAll('.contentBody.video.uad .item[data-video-item]')) .map(item => this._parseItem(item)) }, _pars###b(target) { return Array.from(target.querySelectorAll('#tsukuaso .item')) .map(function(item) { return { type: 'sub', movie: { id: item.querySelector('.itemThumb').dataset.id, title: item.querySelector('.itemTitle a').textContent, }, rootElem: item, } }) }, get _configBarContainer() { return this.doc.querySelector('.column.main') }, createMovieRoot(r###ltOfParsing) { switch (r###ltOfParsing.type) { case 'main': case 'ad': return createMainMovieRoot(r###ltOfParsing.rootElem) case 'sub': return new SubMovieRoot(r###ltOfParsing.rootElem) default: throw new Error(r###ltOfParsing.type) } }, observeMutation(callback) { const nodeList = document.querySelectorAll('.contentBody.video.uad .item.nicoadVideoItem .itemContent') for (const node of Array.from(nodeList)) { new MutationObserver((records, observer) => { for (const r of records) { if (SearchPage._isGettingAdDone(r)) { observer.disconnect() r.target.style.visibility = '' const item = ancestor(r.target, '.item.nicoadVideoItem') callback([SearchPage._parseAdItem(item)]) return } } }).observe(node, { attributes: true, attributeOldValue: true, attributeFilter: ['style'], }) } }, get _pendingMoviesInvisibleCss() { return `.contentBody.video.uad .item, #tsukuaso .item, .contentBody.video.uad .nicoadVideoItemWrapper { visibility: hidden; } .contentBody.video.uad .item[data-video-item-muted], .contentBody.video.uad .item[data-video-item-sensitive], .contentBody.video.uad .item.nrn-thumb-info-done, #tsukuaso .item.nrn-thumb-info-done, .contentBody.video.uad.searchUad .item, .contentBody.video.uad .nicoadVideoItemWrapper.nrn-thumb-info-done, .contentBody.video.uad .nicoadVideoItemWrapper.nrn-thumb-info-done .item { visibility: inherit; } ` }, get css() { return `#nrn-config-bar { margin: 10px 0; } #nrn-config-button, .nrn-visit-button:hover, .nrn-movie-ng-button:hover, .nrn-title-ng-button:hover, .nrn-tag-ng-button:hover, .nrn-contributor-ng-button:hover, .nrn-contributor-ng-id-button:hover, .nrn-contributor-ng-name-button:hover, .nrn-movie-info-toggle:hover, .nrn-description-open-button:hover, .nrn-description-close-button:hover { text-decoration: underline; cursor: pointer; } .nrn-description-open-button { position: absolute; bottom: 0; right: 0; background-color: white; } .nrn-description-text, .nrn-description-close-button { display: block; } .nrn-description-close-button { text-align: right; } .itemData, .itemDescription, .nicoadVideoItemWrapper { position: relative; } .nrn-movie-tag { display: inline-block; margin-right: 1em; } .nrn-description-open-button, .nrn-description-close-button, .nrn-movie-tag-link, .nrn-contributor-link { color: #333333; } .nrn-movie-tag-link.nrn-movie-ng-tag-link, .nrn-contributor-link.nrn-ng-contributor-link, .nrn-matched-ng-contributor-name, .nrn-matched-ng-title { color: white; background-color: fuchsia; } .nrn-movie-info-container .nrn-action-pane { line-height: 1.3em; padding-top: 4px; } .nrn-movie-info-container .nrn-tag-container, .nrn-movie-info-container .nrn-contributor-container { line-height: 1.5em; padding-top: 4px; } .videoList01 .itemContent .itemDescription.ranking.nrn-description { height: auto; width: auto; } .nrn-movie-info-toggle { color: #333333; font-size: 85%; } .videoList01 .nrn-movie-info-toggle { position: absolute; right: 0; top: 0; } .videoList02 .nrn-movie-info-toggle, .nrn-4-column-item .nrn-movie-info-toggle { display: block; text-align: right; } .videoList02 .nrn-movie-info-container { clear: both; } .nrn-hide, .videoList02 .item.nrn-hide, .video .item.nrn-4-column-item.nrn-hide, .uad .nicoadVideoItemWrapper .nicoadVideoItem.nrn-hide, .item[data-video-item-muted] { display: none; } .item.nrn-reduce .videoList01Wrap, .item.nrn-reduce .videoList02Wrap { width: 80px; } .item.nrn-reduce .itemThumbBox, .item.nrn-reduce .itemThumbBox .itemThumb, .item.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap, .item.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap img, .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox, .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb, .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb .itemThumbWrap, .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb .itemThumbWrap img { width: 80px; height: 45px; } .videoList01 .nrn-action-pane, .videoList02 .nrn-action-pane { display: none; position: absolute; top: 0px; right: 0px; padding: 3px; color: #999; background-color: rgb(105, 105, 105); z-index: 11; } .videoList02 .nrn-action-pane { font-size: 85%; } .videoList01 .item:hover .nrn-action-pane, .videoList02 .item:hover .nrn-action-pane, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane { display: block; } .videoList01 .item:hover .nrn-action-pane .nrn-visit-button, .videoList01 .item:hover .nrn-action-pane .nrn-movie-ng-button, .videoList01 .item:hover .nrn-action-pane .nrn-title-ng-button, .videoList02 .item:hover .nrn-action-pane .nrn-visit-button, .videoList02 .item:hover .nrn-action-pane .nrn-movie-ng-button, .videoList02 .item:hover .nrn-action-pane .nrn-title-ng-button, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-visit-button, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-visit-button, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button { color: white; } .videoList01 .item:hover .nrn-action-pane .nrn-movie-ng-button, .videoList01 .item:hover .nrn-action-pane .nrn-title-ng-button, .videoList02 .item:hover .nrn-action-pane .nrn-movie-ng-button, .videoList02 .item:hover .nrn-action-pane .nrn-title-ng-button, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button, .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button, .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button { margin-left: 5px; border-left: solid thin; padding-left: 5px; } .nrn-user-ng-button, .nrn-tag-ng-button { display: inline-block; } .nrn-ng-movie-title, .nrn-contributor-link.nrn-ng-id-contributor-link { text-decoration: line-through; } .nrn-sub-movie-root { position: relative; } .nrn-sub-movie-root .nrn-movie-info-toggle { display: block; text-align: right; background-color: white; } .nrn-sub-movie-root .nrn-movie-info-container { clear: left; padding: 10px 0 15px 0; } .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button, .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button, .nrn-sub-movie-root .nrn-action-pane .nrn-title-ng-button, .nrn-4-column-item .nrn-action-pane .nrn-visit-button, .nrn-4-column-item .nrn-action-pane .nrn-movie-ng-button, .nrn-4-column-item .nrn-action-pane .nrn-title-ng-button { display: inline-block; color: #333333; } .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button, .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button, .nrn-4-column-item .nrn-action-pane .nrn-visit-button, .nrn-4-column-item .nrn-action-pane .nrn-movie-ng-button { margin-right: 0.5em; } .nrn-error { color: red; } .videoList02 .item, .video .item.nrn-4-column-item { float: none; display: inline-block; vertical-align: top; } .video .item.nrn-4-column-item:nth-child(4n+1) { clear: none; } .nrn-4-column-item .nrn-movie-tag { display: block; } ` }, }) Object.assign(SearchPage, { TwoColumnMovieRoot, FourColumnMovieRoot, is(location) { var p = location.pathname return p.startsWith('/search/') || p.startsWith('/tag/') }, _isGettingAdDone(mutationRecord) { const r = mutationRecord return r.attributeName === 'style' && r.oldValue.includes('visibility: hidden;') && r.target.getAttribute('style').includes('visibility: visible;') }, _parseAdItem(item) { const p = item.querySelector('.count.ads .value a').pathname return { type: 'ad', movie: { id: p.slice(p.lastIndexOf('/') + 1), title: item.querySelector('.itemTitle a').textContent, }, rootElem: ancestor(item, '.nicoadVideoItemWrapper'), } }, }) return SearchPage })(NicoPage) var Controller = (function() { var isMovieAnchor = function(e) { return e.dataset.nrnMovieAnchor === 'true' } var movieAnchor = function(child) { for (var n = child; n; n = n.parentNode) { if (n.nodeType !== Node.ELEMENT_NODE) return null if (isMovieAnchor(n)) return n } } var dataOfMovieAnchor = function(e) { return { id: e.dataset.nrnMovieId, title: e.dataset.nrnMovieTitle, } } var Controller = function(config, page) { this.config = config this.page = page } Controller.prototype = { addListenersTo(eventTarget) { eventTarget.addEventListener('change', this._changed.bind(this)) eventTarget.addEventListener('click', this._clicked.bind(this)) }, _changed(event) { switch (event.target.id) { case 'nrn-visited-movie-view-mode-select': this.config.visitedMovieViewMode.value = event.target.value; break case 'nrn-visible-contributor-type-select': this.config.visibleContributorType.value = event.target.value; break case 'nrn-ng-movie-visible-checkbox': this.config.ngMovieVisible.value = event.target.checked; break } }, _addVisitedMovie(target) { var d = dataOfMovieAnchor(movieAnchor(target)) this.config.visitedMovies.addAsync(d.id, d.title) }, _toggleData(target, add, remove) { var ds = target.dataset switch (ds.type) { case 'add': add.call(this, ds); break case 'remove': remove.call(this, ds); break default: throw new Error(ds.type) } }, _toggleVisitedMovie(target) { this._toggleData(target, function(ds) { this.config.visitedMovies.addAsync(ds.movieId, ds.movieTitle) }, function(ds) { this.config.visitedMovies.removeAsync([ds.movieId]) }) }, _toggleNgMovie(target) { this._toggleData(target, function(ds) { this.config.ngMovies.addAsync(ds.movieId, ds.movieTitle) }, function(ds) { this.config.ngMovies.removeAsync([ds.movieId]) }) }, _toggleNgTitle(target) { this._toggleData(target, function(ds) { ConfigDialog.promptNgTitle(this.config, ds.movieTitle) }, function(ds) { this.config.ngTitles.removeAsync([ds.ngTitle]) }) }, _toggleNgTag(target) { this._toggleData(target, function(ds) { if (this.config.addToNgLockedTags.value && ds.lock) { this.config.ngLockedTags.addAsync(ds.tagName); } else { this.config.ngTags.addAsync(ds.tagName); } }, function(ds) { this.config.ngTags.removeAsync([ds.tagName]) this.config.ngLockedTags.removeAsync([ds.tagName]) }) }, _toggleContributorNgId(target) { var name = function(ds) { return Contributor.new(ds.contributorType, ds.id, ds.name).ngIdStoreName } this._toggleData(target, function(ds) { this.config[name(ds)].addAsync(parseInt(ds.id), ds.name) }, function(ds) { this.config[name(ds)].removeAsync([parseInt(ds.id)]) }) }, _toggleNgUserName(target) { this._toggleData(target, function(ds) { ConfigDialog.promptNgUserName(this.config, ds.name) }, function(ds) { this.config.ngUserNames.removeAsync([ds.matched]) }) }, _clicked(event) { var e = event.target if (e.id === 'nrn-config-button') { this.page.showConfigDialog(this.config) } else if (movieAnchor(e)) { this._addVisitedMovie(e) } else if (e.classList.contains('nrn-visit-button')) { this._toggleVisitedMovie(e) } else if (e.classList.contains('nrn-movie-ng-button')) { this._toggleNgMovie(e) } else if (e.classList.contains('nrn-title-ng-button')) { this._toggleNgTitle(e) } else if (e.classList.contains('nrn-movie-info-toggle')) { this.page.getMovieRootBy(e).toggleMovieInfo() } else if (e.classList.contains('nrn-description-open-button') || e.classList.contains('nrn-description-close-button')) { this.page.getMovieRootBy(e).toggleDescription() } else if (e.classList.contains('nrn-tag-ng-button')) { this._toggleNgTag(e) } else if (e.classList.contains('nrn-contributor-ng-button')) { this._toggleContributorNgId(e) } else if (e.classList.contains('nrn-contributor-ng-id-button')) { this._toggleContributorNgId(e) } else if (e.classList.contains('nrn-contributor-ng-name-button')) { this._toggleNgUserName(e) } }, } return Controller })() var Main = (function() { var createMovieRoot = function(r###ltOfParsing, page, movieViewMode) { var movie = movieViewMode.movie var r###lt = page.createMovieRoot(r###ltOfParsing) r###lt.actionPane = new NicoPage.ActionPane(page.doc, movie).bindToMovie(movie) r###lt.setMovieInfoToggleIfRequired() r###lt.markMovieAnchor() r###lt.id = movie.id r###lt.title = movie.title r###lt.bindToMovieViewMode(movieViewMode) r###lt.bindToConfig(movieViewMode.config) r###lt.bindToMovie(movie) return r###lt } var createMovieRoots = function(r###ltsOfParsing, model, page) { for (var r of r###ltsOfParsing) { var movie = model.movies.get(r.movie.id) var movieViewMode = model.movieViewModes.get(movie) var root = createMovieRoot(r, page, movieViewMode) root.movieTitle = new NicoPage.MovieTitle(root.titleElem).bindToMovie(movie) page.mapToggleTo(root) } } var setup = function(r###ltsOfParsing, model, page) { model.createMovies(r###ltsOfParsing) createMovieRoots(r###ltsOfParsing, model, page) } var createMessageElem = function(doc, message) { var r###lt = doc.createElement('p') r###lt.textContent = message return r###lt } function gmXmlHttpRequest() { if (typeof GM_xmlhttpRequest === 'undefined') return GM.xmlHttpRequest return GM_xmlhttpRequest } var createThumbInfoRequester = function(movies, movieViewModes) { var thumbInfo = new ThumbInfo(gmXmlHttpRequest()) .on('completed', ThumbInfoListener.forCompleted(movies)) .on('errorOccurred', ThumbInfoListener.forErrorOccurred(movies)) return function(prefer) { thumbInfo.request( movieViewModes.sort().map(function(m) { return m.movie.id }), prefer) } } var getThumbInfoRequester = function(movies, movieViewModes) { return movies.config.useGetThumbInfo.value ? createThumbInfoRequester(movies, movieViewModes) : function() {} } var createModel = function(config) { var movies = new Movies(config) var movieViewModes = new MovieViewModes(config) var requestThumbInfo = getThumbInfoRequester(movies, movieViewModes) return { config, movies, movieViewModes, requestThumbInfo, createMovies(r###ltsOfParsing) { movies.setIfAbsent(r###ltsOfParsing.map(function(r) { return new Movie(r.movie.id, r.movie.title) })) }, } } var createView = function(page) { var configBar = page.createConfigBar() return { page, addConfigBar() { page.addConfigBar(configBar) }, _bindToConfig(config) { page.bindToConfig(config) configBar.bindToConfig(config) }, bindToModel(model) { this._bindToConfig(model.config) }, bindToWindow() { }, setup(model, targetElem) { setup(page.parse(targetElem), model, page) }, setupAndRequestThumbInfo(model, targetElem) { this.setup(model, targetElem) model.requestThumbInfo() }, observeMutation(model) { page.observeMutation(function(r###ltOfParsing, prefer) { setup(r###ltOfParsing, model, page) model.requestThumbInfo(prefer) }) }, } } function addStyle(style) { const e = document.createElement('style'); e.textContent = style; document.head.appendChild(e); } function gmGetValue() { if (typeof GM_getValue === 'undefined') return GM.getValue return GM_getValue } function gmSetValue() { if (typeof GM_setValue === 'undefined') return GM.setValue return GM_setValue } function handleAutoPagerizedNodes(model, page) { return function(e) { const t = e.target if (t.nodeType === Node.ELEMENT_NODE && t.classList.contains('item')) { setup(page.parseAutoPagerizedNodes(t), model, page) model.requestThumbInfo() } } } var domContentLoaded = function(page) { return async function() { try { addStyle(page.css) const config = new Config(gmGetValue(), gmSetValue()) await config.sync() var model = createModel(config) var view = createView(page) view.addConfigBar() view.bindToModel(model) view.bindToWindow() view.setupAndRequestThumbInfo(model) view.observeMutation(model) new Controller(model.config, page).addListenersTo(page.doc.body) if (!model.config.useGetThumbInfo.value) { page.pendingMoviesVisible = true } page.doc.body.addEventListener('AutoPagerize_DOMNodeInserted', handleAutoPagerizedNodes(model, page)) } catch (e) { console.error(e) page.pendingMoviesVisible = true } } } var getPage = function() { if (SearchPage.is(document.location)) return new SearchPage(document) return new ListPage(document) } var main = function() { var page = getPage() page.pendingMoviesVisible = false if (['interactive', 'complete'].includes(document.readyState)) domContentLoaded(page)() else page.doc.addEventListener('DOMContentLoaded', domContentLoaded(page)) } return {main} })() Main.main() })()