// ==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 56
// @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 result = []
result[TYPE] = type
result[VALUE] = value
result[TEXT] = text
return result
}
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 result = createTypeToValuesMap()
for (var r of records(csv)) {
var values = result.get(r[TYPE])
if (values) values.push(getValue(r))
}
return result
},
}
})()
var Config = function(getValue, setValue) {
var store = function(key, defaultValue) {
return new Store(getValue, setValue, key, defaultValue || true)
}
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')
this.useGetThumbInfo = store('useGetThumbInfo')
this.movieInfoTogglable = store('movieInfoTogglable')
this.descriptionTogglable = store('descriptionTogglable')
this.seamlessRankingNumber = store('seamlessRankingNumber')
this.requestingNext = store('requestingNext')
this.popupVisible = store('popupVisible')
this.movingUp = store('movingUp')
this.visitedMovies = arrayStore('visitedMovies')
this.ngMovies = arrayStore('ngMovies')
this.ngTitles = arrayStore('ngTitles', true)
this.ngTags = arrayStore('ngTags', true)
this.ngUserIds = arrayStore('ngUserIds')
this.ngUserNames = arrayStore('ngUserNames', true)
this.ngChannelIds = arrayStore('ngChannelIds')
this.ngMovieVisible = ngMovieVisibleStore()
}
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.seamlessRankingNumber.sync(),
this.requestingNext.sync(),
this.popupVisible.sync(),
this.movingUp.sync(),
this.visitedMovies.sync(),
this.ngMovies.sync(),
this.ngTitles.sync(),
this.ngTags.sync(),
this.ngUserIds.sync(),
this.ngUserNames.sync(),
this.ngChannelIds.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) {
var parseTags = function(tags) {
return Array.from(tags, function(t) { return t.textContent })
}
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 result = {error: {type, message}}
if (id) result.id = id
return result
}
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(name) {
_super.call(this)
this.name = name
this.ng = false
}
Tag.prototype = createObject(_super.prototype, {
updateNg(upperCaseNgTagNameSet) {
var pre = this.ng
this.ng = 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 function(name) {
if (map.has(name)) return map.get(name)
var tag = new Tag(name)
map.set(name, tag)
config.ngTags.on('changed', tag.updateNg.bind(tag))
return tag
}
}
var createTagsBuilder = function(config) {
var getTagBy = createTagBuilder(config)
return function(tagNames) {
var tags = tagNames.map(getTagBy)
var ngTagSet = config.ngTags.set
for (var t of tags) t.updateNg(ngTagSet)
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-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')
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%;
}
</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-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><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 result = this.elem.ownerDocument.createElement('span')
result.className = 'nrn-matched-ng-title'
result.textContent = textContent
return result
},
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 result = doc.createElement('span')
result.className = 'nrn-visit-button'
result.textContent = '閲覧済み'
result.dataset.movieId = movie.id
result.dataset.type = 'add'
result.dataset.movieTitle = movie.title
return result
}
var createMovieNgButton = function(doc, movie) {
var result = doc.createElement('span')
result.className = 'nrn-movie-ng-button'
result.textContent = 'NG動画'
result.dataset.movieId = movie.id
result.dataset.type = 'add'
result.dataset.movieTitle = movie.title
return result
}
var createTitleNgButton = function(doc, movie) {
var result = doc.createElement('span')
result.className = 'nrn-title-ng-button'
result.textContent = 'NGタイトル追加'
result.dataset.movieTitle = movie.title
result.dataset.ngTitle = ''
return result
}
var createPane = function(doc) {
var result = doc.createElement('div')
result.className = 'nrn-action-pane'
for (var c of Array.from(arguments).slice(1)) result.appendChild(c)
return result
}
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, tagName) {
var a = doc.createElement('a')
a.className = 'nrn-movie-tag-link'
a.target = '_blank'
a.textContent = tagName
a.href = 'https://www.nicovideo.jp/tag/' + tagName
var b = doc.createElement('span')
b.className = 'nrn-tag-ng-button'
b.textContent = '[+]'
b.dataset.type = 'add'
b.dataset.tagName = tagName
var result = doc.createElement('span')
result.className = 'nrn-movie-tag'
result.appendChild(a)
result.appendChild(b)
return result
}
var TagView = function(doc, tagName) {
this.tagName = tagName
this.elem = createElem(doc, tagName)
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 result = doc.createElement('span')
result.className = 'nrn-contributor'
result.appendChild(doc.createTextNode(this._label))
result.appendChild(a)
result.appendChild(b)
return result
},
_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)
b.appendChild(d.createTextNode('/'))
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 result = this.elem.ownerDocument.createElement('span')
result.className = 'nrn-matched-ng-contributor-name'
result.textContent = text
return result
}.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 result = doc.createElement('div')
result.className = 'nrn-movie-info-container'
result.appendChild(e)
result.appendChild(t)
result.appendChild(c)
return result
}
var createToggle = function(doc) {
var result = doc.createElement('span')
result.className = 'nrn-movie-info-toggle'
result.textContent = TOGGLE_OPEN_TEXT
return result
}
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.name).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 result = doc.createElement('span')
result.className = 'nrn-description-close-button'
result.textContent = TOGGLE_CLOSE_TEXT
return result
}
var createElem = function(doc, closeButton) {
var text = doc.createElement('span')
text.className = 'nrn-description-text'
var result = doc.createElement('p')
result.className = 'itemDescription ranking nrn-description'
result.appendChild(text)
result.appendChild(closeButton)
return result
}
var createOpenButton = function(doc) {
var result = doc.createElement('span')
result.className = 'nrn-description-open-button'
result.textContent = TOGGLE_OPEN_TEXT
return result
}
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 result = []
for (var s of this._movieAnchorSelectors) {
var a = this.elem.querySelector(s)
if (a) result.push(a)
}
return result
},
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 result = this.description.original
if (!result) {
result
= this.description.original
= this._queryOriginalDescriptionElem()
}
return result
},
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 result = e.firstChild
result.remove()
return result
}
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 result = this.doc.createElement('style')
result.id = 'nrn-pending-movies-hide-style'
result.textContent = this._pendingMoviesInvisibleCss
return result
},
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(resultOfParsing) {
switch (resultOfParsing.type) {
case 'main': return new MovieRoot(resultOfParsing.rootElem)
case 'sub': return new SubMovieRoot(resultOfParsing.rootElem)
case 'ad': return new AdMovieRoot(resultOfParsing.rootElem)
default: throw new Error(resultOfParsing.type)
}
},
get _configBarContainer() {
return this.doc.querySelector('.RankingVideoListContainer')
},
parse(target) {
target = target || this.doc
return this._parseMain(target).concat(this._parseSub(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));
},
_parseSub(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._parseSub(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))
},
_parseSub(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(resultOfParsing) {
switch (resultOfParsing.type) {
case 'main':
case 'ad':
return createMainMovieRoot(resultOfParsing.rootElem)
case 'sub':
return new SubMovieRoot(resultOfParsing.rootElem)
default:
throw new Error(resultOfParsing.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
case 'nrn-moving-up-checkbox':
this.config.movingUp.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) {
this.config.ngTags.addAsync(ds.tagName)
}, function(ds) {
this.config.ngTags.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 LazyImageLoader = (function() {
var LazyImageLoader = function(doc) {
this.doc = doc
this._requested = false
}
LazyImageLoader.prototype = {
_lazyImages() {
return Array.from(this.doc.querySelectorAll('.thumb.jsLazyImage'))
},
_lazyImagesInView() {
return this._lazyImages().filter(this._isInView.bind(this))
},
_isInView(elem) {
var r = elem.getBoundingClientRect()
if (!(r.width && r.height)) return false
var h = this.doc.defaultView.innerHeight
return (r.top >= 0 && r.top < h) || (r.bottom > 0 && r.bottom <= h)
},
_load() {
this._lazyImagesInView().forEach(function(img) {
img.src = img.dataset.original
img.dataset.original = ''
img.classList.remove('jsLazyImage')
})
},
request() {
if (this._requested) return
this._requested = true
setTimeout(function() {
this._requested = false
this._load()
}.bind(this), 125)
},
}
return LazyImageLoader
})()
var Main = (function() {
var createMovieRoot = function(resultOfParsing, page, movieViewMode) {
var movie = movieViewMode.movie
var result = page.createMovieRoot(resultOfParsing)
result.actionPane
= new NicoPage.ActionPane(page.doc, movie).bindToMovie(movie)
result.setMovieInfoToggleIfRequired()
result.markMovieAnchor()
result.id = movie.id
result.title = movie.title
result.bindToMovieViewMode(movieViewMode)
result.bindToConfig(movieViewMode.config)
result.bindToMovie(movie)
return result
}
var createMovieRoots = function(resultsOfParsing, model, page) {
for (var r of resultsOfParsing) {
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 createLazyImageLoader = function(doc) {
var loader = new LazyImageLoader(doc)
return loader.request.bind(loader)
}
var setup = function(resultsOfParsing, model, page) {
model.createMovies(resultsOfParsing)
createMovieRoots(resultsOfParsing, model, page)
}
var createMessageElem = function(doc, message) {
var result = doc.createElement('p')
result.textContent = message
return result
}
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(resultsOfParsing) {
movies.setIfAbsent(resultsOfParsing.map(function(r) {
return new Movie(r.movie.id, r.movie.title)
}))
},
}
}
var createView = function(page) {
var loadLazyImages = createLazyImageLoader(page.doc)
var configBar = page.createConfigBar()
return {
page,
loadLazyImages,
addConfigBar() {
page.addConfigBar(configBar)
},
_bindToConfig(config) {
page.bindToConfig(config)
configBar.bindToConfig(config)
config.movingUp.on('changed', loadLazyImages)
},
bindToModel(model) {
this._bindToConfig(model.config)
model.movieViewModes.on('movieViewModeChanged'
, loadLazyImages)
},
bindToWindow() {
page.doc.defaultView.addEventListener('scroll', loadLazyImages)
},
setup(model, targetElem) {
setup(page.parse(targetElem), model, page)
},
setupAndRequestThumbInfo(model, targetElem) {
this.setup(model, targetElem)
model.requestThumbInfo()
},
observeMutation(model) {
page.observeMutation(function(resultOfParsing, prefer) {
setup(resultOfParsing, 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()
})()