// ==UserScript==
// @name Nico Nico Ranking NG
// @namespace http://userscripts.org/users/121129
// @description ニコニコ動画のランキングにNG機能を追加
// @match http://www.nicovideo.jp/ranking*
// @version 14
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_addStyle
// @license MIT License
// @noframes
// ==/UserScript==
;(function() {
'use strict'
var array = [].slice.call.bind([].slice)
var noop = function() {}
var toUpperCase = ''.toUpperCase.call.bind(''.toUpperCase)
var getter = function(propName) {
return function() { return this[propName] }
}
var always = function(v) {
return function() { return v }
}
var not = function(fn) {
return function() { return !fn.apply(null, arguments) }
}
var curry = (function() {
var applyOrRebind = function(func, arity, args) {
var passed = args.concat(array(arguments, 3)).slice(0, arity)
return arity === passed.length
? func.apply(null, passed)
: applyOrRebind.bind(null, func, arity, passed)
}
return function(func) {
return applyOrRebind.bind(null, func, func.length, [])
}
})()
var eq = curry(function(a, b) { return a === b })
var prop = curry(function(propName, obj) {
return obj[propName]
})
var callMethod = curry(function(methodName, args, obj) {
return obj[methodName].apply(obj, args)
})
var flip = function(fn) {
return curry(function(a, b) { return fn.call(null, b, a) })
}
var include = curry(function(array, elem) {
return array.indexOf(elem) >= 0
})
var includeUpperCase = curry(function(array, elem) {
return include(array, elem.toUpperCase())
})
var compose = function() {
var args = array(arguments)
var first = args.pop()
return function() {
return args.reduceRight(function(v, f) {
return f(v)
}, first.apply(null, arguments))
}
}
var removeAllChild = function(parent) {
while (parent.firstChild) parent.removeChild(parent.firstChild)
}
var removeFromParent = function(elem) {
var p = elem.parentNode
if (p) p.removeChild(elem)
}
var ancestorAnchor = function(elem) {
for (var a = elem; a; a = a.parentNode) if (a.tagName === 'A') return a
return null
}
var elem = (function() {
var setter = function(mapName) {
return function() {
if (arguments.length === 1) {
var o = arguments[0] || {}
Object.keys(o).forEach(function(k) { this[mapName][k] = o[k] }, this)
} else if (arguments.length === 2) {
this[mapName][arguments[0]] = arguments[1]
}
return this
}
}
var Builder = function(tagName) {
this.tagName = tagName
this.attrMap = Object.create(null)
this.cssMap = Object.create(null)
this.handlers = []
this.children = []
}
Builder.prototype.attr = setter('attrMap')
Builder.prototype.css = setter('cssMap')
Builder.prototype.on = function(type, handler, capture) {
this.handlers.push({
type: type,
handler: handler,
capture: Boolean(capture),
})
return this
}
Builder.prototype.add = function() {
this.children = [].concat.apply(this.children, arguments)
return this
}
Builder.prototype.new = function(doc) {
doc = doc || document
var result = this.tagName ? doc.createElement(this.tagName)
: doc.createDocumentFragment()
elem.attr(result, this.attrMap)
elem.css(result, this.cssMap)
elem.add(result, this.children)
this.handlers.forEach(function(h) {
result.addEventListener(h.type, h.handler, h.capture)
})
return result
}
var elem = function(tagName) {
return new Builder(tagName)
}
elem.attr = function(elem) {
var l = arguments.length
if (l === 3) {
elem.setAttribute(arguments[1], arguments[2])
} else if (l === 2) {
var o = arguments[1] || {}
Object.keys(o).forEach(function(k) {
var v = o[k]
if (['string', 'number'].indexOf(typeof v) >= 0) {
elem.setAttribute(k, o[k])
} else {
elem[k] = Boolean(v)
}
})
}
return elem
}
elem.css = function(elem) {
var l = arguments.length
if (l === 3) {
elem.style.setProperty(arguments[1], arguments[2], null)
} else if (l === 2) {
var o = arguments[1] || {}
Object.keys(o).forEach(function(k) {
elem.style.setProperty(k, o[k], null)
})
}
return elem
}
elem.add = function(elem) {
var children = [].concat.apply([], [].slice.call(arguments, 1))
var d = elem.ownerDocument
var f = d.createDocumentFragment()
children.map(function(c) {
return c.nodeType ? c : d.createTextNode(c)
}).forEach(f.appendChild.bind(f))
elem.appendChild(f)
return elem
}
return elem
})()
var newStores = function() {
var includeCaseInsensitive = function(array, elem) {
return includeUpperCase(array.map(toUpperCase), elem)
}
var filterCaseInsensitive = function(array, elems) {
return array.filter(not(includeUpperCase(elems.map(toUpperCase))))
}
var o = function(key, defVal, json, caseInsensitive) {
return {
get: GM_getValue,
set: GM_setValue,
clear: GM_deleteValue,
key: key,
defaultValue: defVal,
json: json,
include: caseInsensitive ? includeCaseInsensitive : undefined,
filter: caseInsensitive ? filterCaseInsensitive : undefined,
}
}
return {
visitedMovieViewMode: new Store(o('visitedMovieViewMode', 'reduce')),
openNewWindow: new Store(o('openNewWindow', true)),
useGetThumbInfo: new Store(o('useGetThumbInfo', true)),
movieInfoTogglable: new Store(o('movieInfoTogglable', true)),
descriptionTogglable: new Store(o('descriptionTogglable', true)),
visitedMovieIds: new Store(o('visitedMovies', [], true)),
ngMovieIds: new Store(o('ngMovies', [], true)),
ngTitles: new Store(o('ngTitles', [], true, true)),
ngTags: new Store(o('ngTags', [], true, true)),
ngUserIds: new Store(o('ngUserIds', [], true)),
ngChannelIds: new Store(o('ngChannelIds', [], true)),
}
}
var updateMovies = function(movies, stores) {
var visitedMovieIds = stores.visitedMovieIds.get()
var ngMovieIds = stores.ngMovieIds.get()
var ngTitles = stores.ngTitles.get()
movies.forEach(function(movie) {
movie.setVisitedIfInclude(visitedMovieIds)
movie.setNgIdIfInclude(ngMovieIds)
movie.setNgTitleIfInclude(ngTitles)
})
}
var commonCssText = function() {
return [
'#nrn-config-button {',
' text-decoration: underline;',
' cursor: pointer;',
'}',
'.nrn-popup {',
' display: none;',
' position: absolute;',
' top: 10px;',
' right: 0px;',
' padding: 3px;',
' color: #999;',
' background-color: rgb(105, 105, 105);',
'}',
'.nrn-popup span {',
' color: white;',
'}',
'.nrn-popup span:hover {',
' text-decoration: underline;',
' cursor: pointer;',
'}',
'.nrn-display-none {',
' display: none;',
'}',
'.nrn-matched-ng-title {',
' color: white;',
' background-color: fuchsia;',
'}',
'.nrn-movie-info-container .nrn-movie-info-p {',
' margin-top: 4px;',
' line-height: 1.5em;',
'}',
'.nrn-tag-ng-button,',
'.nrn-contributor-ng-button {',
' cursor: pointer;',
'}',
].join('\n')
}
var Movie = (function() {
var setIfIncludeId = function(targetProp, viewMethod) {
return function(ids) {
var b = include(ids, this._id)
if (this[targetProp] !== b) {
this[targetProp] = b
this._view[viewMethod](this)
}
}
}
var setNgContributorIfInclude = function(ids) {
var ng = include(ids, this._id)
if (this._ng !== ng) {
this._ng = ng
this._view.updateNgContributor(this._movie)
}
}
var equalArray = function(a1, a2) {
if (a1.length !== a2.length) return false
for (var i = 0; i < a1.length; i++)
if (a1[i] !== a2[i]) return false
return true
}
var compressU3000 = function(str) {
return str.replace(/\u3000{2,}/g, '\u3000')
}
var Contributor = function(id, name, view, movie) {
this._id = id
this._name = name
this._ng = false
this._view = view
this._movie = movie
}
Contributor.prototype.id = getter('_id')
Contributor.prototype.name = getter('_name')
Contributor.prototype.isNg = getter('_ng')
Contributor.prototype.isUser = always(false)
Contributor.prototype.isChannel = always(false)
Contributor.prototype.setNgUserIfInclude = noop
Contributor.prototype.setNgChannelIfInclude = noop
Contributor.null = new Contributor(-1, '', null, null)
var User = function() {
Contributor.apply(this, arguments)
}
User.prototype = Object.create(Contributor.prototype)
User.prototype.isUser = always(true)
User.prototype.setNgUserIfInclude = setNgContributorIfInclude
var Channel = function() {
Contributor.apply(this, arguments)
}
Channel.prototype = Object.create(Contributor.prototype)
Channel.prototype.isChannel = always(true)
Channel.prototype.setNgChannelIfInclude = setNgContributorIfInclude
var Movie = function(id, title, view) {
this._id = id
this._title = title
this._view = view
this._visited = false
this._ngId = false
this._ngTitle = false
this._matchedNgTitle = ''
this._tags = []
this._ngTags = []
this._error = ''
this._description = ''
this._contributor = Contributor.null
}
Movie.prototype.id = getter('_id')
Movie.prototype.title = getter('_title')
Movie.prototype.isVisited = getter('_visited')
Movie.prototype.setVisitedIfInclude =
setIfIncludeId('_visited', 'updateVisited')
Movie.prototype.isNgId = getter('_ngId')
Movie.prototype.setNgIdIfInclude = setIfIncludeId('_ngId', 'updateNgId')
Movie.prototype.isNgTitle = getter('_ngTitle')
Movie.prototype.matchedNgTitle = getter('_matchedNgTitle')
Movie.prototype._matchedNgTitleIfInclude = function(ngTitles) {
var matched = ngTitles.filter(
includeUpperCase(this._title.toUpperCase()))
return matched.length ? matched[0] : ''
}
Movie.prototype._viewUpdateMethodName = function(newMatchedNgTitle) {
var newNgTitle = Boolean(newMatchedNgTitle)
if (this._ngTitle !== newNgTitle) {
return 'updateNgTitle'
}
if (this._ngTitle && newNgTitle
&& this._matchedNgTitle !== newMatchedNgTitle) {
return 'updateMatchedNgTitle'
}
return ''
}
Movie.prototype.setNgTitleIfInclude = function(ngTitles) {
var matchedNgTitle = this._matchedNgTitleIfInclude(ngTitles)
var updateMethodName = this._viewUpdateMethodName(matchedNgTitle)
this._ngTitle = Boolean(matchedNgTitle)
this._matchedNgTitle = matchedNgTitle
if (updateMethodName) this._view[updateMethodName](this)
}
Movie.prototype.isNG = function() {
return this._ngId
|| this._ngTitle
|| Boolean(this._ngTags.length)
|| this._contributor.isNg()
}
Movie.prototype.getTags = getter('_tags')
Movie.prototype.getNgTags = getter('_ngTags')
Movie.prototype.setNgTagsIfMatch = function(ngTags) {
var newNgTags = ngTags.filter(
includeUpperCase(this._tags.map(toUpperCase)))
if (!equalArray(this._ngTags, newNgTags)) {
this._ngTags = newNgTags
this._view.updateNgTags(this)
}
}
Movie.prototype.hasNgTag = function(tag) {
return this._ngTags.map(toUpperCase).some(eq(tag.toUpperCase()))
}
Movie.prototype.setNgUserIfInclude = function(ids) {
this._contributor.setNgUserIfInclude(ids)
}
Movie.prototype.setNgChannelIfInclude = function(ids) {
this._contributor.setNgChannelIfInclude(ids)
}
Movie.prototype.hasMovieInfo = function() {
return Boolean(this._tags.length)
|| this._contributor !== Contributor.null
}
Movie.prototype.contributor = getter('_contributor')
Movie.prototype._newContributorBy = function(thumbInfo) {
var c = thumbInfo.contributor
switch (c.type) {
case 'user': return new User(c.id, c.name, this._view, this)
case 'channel': return new Channel(c.id, c.name, this._view, this)
}
}
Movie.prototype.getError = getter('_error')
Movie.prototype.getDescription = getter('_description')
Movie.prototype.setThumbInfo = function(thumbInfo) {
if (thumbInfo.error) {
this._error = thumbInfo.error
} else {
this._description = compressU3000(thumbInfo.description)
this._tags = thumbInfo.tags
this._contributor = this._newContributorBy(thumbInfo)
if (this._title !== thumbInfo.title) {
this.setNgTitleIfInclude([])
this._title = thumbInfo.title
this._view.updateTitle(this)
}
}
this._view.updateThumbInfo(this)
}
return Movie
})()
var Store = (function() {
var defaultInclude = include
var defaultFilter = function(array, elems) {
return array.filter(not(include(elems)))
}
var Store = function(obj) {
this._get = obj.get
this._set = obj.set
this._clear = obj.clear
this._key = obj.key
this._defaultValue = obj.json ? JSON.stringify(obj.defaultValue)
: obj.defaultValue
this._json = Boolean(obj.json)
this._include = obj.include || defaultInclude
this._filter = obj.filter || defaultFilter
this._cache = null
}
Store.prototype.get = function() {
var v = this._get(this._key, this._defaultValue)
return this._cache = (this._json ? JSON.parse(v) : v)
}
Store.prototype.cacheOrGet = function() {
return this._cache === null ? (this._cache = this.get()) : this._cache
}
Store.prototype._setValue = function(value) {
this._set(this._key, this._json ? JSON.stringify(value) : value)
this._cache = null
return true
}
Store.prototype.set = function(value) {
return value === this.get() ? false : this._setValue(value)
}
Store.prototype.clear = function() {
this._clear(this._key)
this._cache = null
}
Store.prototype.add = function(elem) {
var array = this.get()
if (this._include(array, elem)) return false
return this._setValue(array.concat(elem))
}
Store.prototype.remove = function(/* ...elem */) {
var oldArray = this.get()
var newArray = this._filter(oldArray, [].concat.apply([], arguments))
if (oldArray.length === newArray.length) return false
return this._setValue(newArray)
}
return Store
})()
var ViewModeStore = function(store) {
this._store = store
}
ViewModeStore.prototype.get = function() {
return ViewMode.get(this._store.get())
}
ViewModeStore.prototype.set = function(viewMode) {
this._store.set(viewMode.name())
}
var ObservableStore = function(store, postChange) {
this._store = store
this._postChange = postChange
}
ObservableStore.prototype.get = function() {
return this._store.get()
}
ObservableStore.prototype.set = function(value) {
if (this._store.set(value)) this._postChange(this)
}
ObservableStore.prototype.clear = function() {
this._store.clear()
this._postChange(this)
}
ObservableStore.prototype.add = function(elem) {
if (this._store.add(elem)) {
this._postChange(this)
return true
}
return false
}
ObservableStore.prototype.remove = function(/* ...elem */) {
var s = this._store
if (s.remove.apply(s, arguments)) this._postChange(this)
}
var GetThumbInfo = (function() {
var parseTags = function(tags) {
return [].map.call(tags, prop('textContent'))
}
var contributor = function(rootElem, type, id, name) {
return {
type: type,
id: parseInt(rootElem.querySelector(id).textContent, 10),
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,
}
}
var parseError = function(rootElem) {
switch (rootElem.querySelector('error > code').textContent) {
case 'DELETED': return {error: '削除された動画'}
case 'NOT_FOUND': return {error: '見つからない、または無効な動画'}
case 'COMMUNITY': return {error: 'コミュニティ限定動画'}
}
}
var parseResText = function(resText) {
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)
}
}
var GetThumbInfo = function(request, movies, stores, concurrent) {
this._request = request
this._movies = movies
this._concurrent = concurrent
this._stores = stores
}
GetThumbInfo.prototype._onerror = function(remainder, movie, message) {
this._requestMovie(remainder, remainder.shift())
movie.setThumbInfo({error: message})
}
GetThumbInfo.prototype._setNg = function(movie) {
movie.setNgTagsIfMatch(this._stores.ngTags.cacheOrGet())
movie.setNgUserIfInclude(this._stores.ngUserIds.cacheOrGet())
movie.setNgChannelIfInclude(this._stores.ngChannelIds.cacheOrGet())
movie.setNgTitleIfInclude(this._stores.ngTitles.cacheOrGet())
}
GetThumbInfo.prototype._onload = function(remainder, movie, res) {
this._requestMovie(remainder, remainder.shift())
if (res.status === 200) {
var thumbInfo = parseResText(res.responseText)
movie.setThumbInfo(thumbInfo)
if (!thumbInfo.error) this._setNg(movie)
} else {
movie.setThumbInfo({error: res.statusText})
}
}
GetThumbInfo.prototype._requestMovie = function(remainder, movie) {
if (!movie) return
this._request({
method: 'GET',
url: 'http://ext.nicovideo.jp/api/getthumbinfo/' + movie.id(),
timeout: 5000,
onload: this._onload.bind(this, remainder, movie),
onerror: this._onerror.bind(this, remainder, movie, 'エラー'),
ontimeout: this._onerror.bind(this, remainder, movie, 'タイムアウト'),
})
}
GetThumbInfo.prototype.request = function() {
var remainder = this._movies.slice(this._concurrent)
this._movies.slice(0, this._concurrent)
.forEach(this._requestMovie.bind(this, remainder))
}
return GetThumbInfo
})()
var Model = (function() {
var movieUpdater = function(movies, setter) {
var flippedSetterCall = flip(setter.call.bind(setter))
return function(store) {
movies.forEach(flippedSetterCall(store.get()))
}
}
var Model = function(movies, view, stores) {
this._movies = movies
this._view = view
this._ngMovieVisible = false
this._useGetThumbInfo = stores.useGetThumbInfo
this._visitedMovieViewMode =
new ViewModeStore(
new ObservableStore(stores.visitedMovieViewMode
, view.updateVisitedMovieViewMode.bind(view)))
this._openNewWindow =
new ObservableStore(stores.openNewWindow
, view.updateNewWindowOpen.bind(view))
this._movieInfoTogglable =
new ObservableStore(stores.movieInfoTogglable
, view.updateMovieInfoTogglable.bind(view))
this._descriptionTogglable =
new ObservableStore(stores.descriptionTogglable
, view.updateDescriptionTogglable.bind(view))
var p = Movie.prototype
this._visitedMovieIds =
new ObservableStore(stores.visitedMovieIds
, movieUpdater(movies, p.setVisitedIfInclude))
this._ngMovieIds =
new ObservableStore(stores.ngMovieIds
, movieUpdater(movies, p.setNgIdIfInclude))
this._ngTitles =
new ObservableStore(stores.ngTitles
, movieUpdater(movies, p.setNgTitleIfInclude))
this._ngTags =
new ObservableStore(stores.ngTags
, movieUpdater(movies, p.setNgTagsIfMatch))
this._ngUserIds =
new ObservableStore(stores.ngUserIds
, movieUpdater(movies, p.setNgUserIfInclude))
this._ngChannelIds =
new ObservableStore(stores.ngChannelIds
, movieUpdater(movies, p.setNgChannelIfInclude))
}
Model.prototype.movies = getter('_movies')
Model.prototype.movieById = function(id) {
var a = this._movies.filter(compose(eq(id), callMethod('id', [])))
return a.length ? a[0] : null
}
Model.prototype.isNgMovieVisible = getter('_ngMovieVisible')
Model.prototype.setNgMovieVisible = function(ngMovieVisible) {
if (this._ngMovieVisible !== ngMovieVisible) {
this._ngMovieVisible = ngMovieVisible
this._view.updateNgMovieVisible()
}
}
Model.prototype.visitedMovieViewMode = getter('_visitedMovieViewMode')
Model.prototype.openNewWindow = getter('_openNewWindow')
Model.prototype.useGetThumbInfo = getter('_useGetThumbInfo')
Model.prototype.movieInfoTogglable = getter('_movieInfoTogglable')
Model.prototype.descriptionTogglable = getter('_descriptionTogglable')
Model.prototype.visitedMovieIds = getter('_visitedMovieIds')
Model.prototype.ngMovieIds = getter('_ngMovieIds')
Model.prototype.ngTitles = getter('_ngTitles')
Model.prototype.ngTags = getter('_ngTags')
Model.prototype.ngUserIds = getter('_ngUserIds')
Model.prototype.ngChannelIds = getter('_ngChannelIds')
Model.prototype._isVisible = function(movie) {
return !movie.isNG() || this._ngMovieVisible
}
Model.prototype._isHidden = function(movie) {
return !this._isVisible(movie)
|| (movie.isVisited()
&& this.visitedMovieViewMode().get().isHideMode())
}
Model.prototype._isReduced = function(movie) {
return this._isVisible(movie)
&& movie.isVisited()
&& this.visitedMovieViewMode().get().isReduceMode()
}
Model.prototype.movieViewMode = function(movie) {
if (this._isHidden(movie)) return new ViewMode.Hide()
if (this._isReduced(movie)) return new ViewMode.Reduce()
return new ViewMode.DoNothing()
}
Model.prototype.sortMoviesByVisible = function() {
var self = this
return this._movies.map(function(movie, i) {
return { rank: i, movie: movie }
}).sort(function(a, b) {
var aIsHidden = self._isHidden(a.movie)
var bIsHidden = self._isHidden(b.movie)
if (!aIsHidden && bIsHidden) return -1
if (aIsHidden && !bIsHidden) return 1
if (a.rank < b.rank) return -1
if (a.rank > b.rank) return 1
return 0
}).map(prop('movie'))
}
return Model
})()
var ViewMode = function() {}
ViewMode.get = function(name) {
switch (name) {
case ViewMode.DoNothing.prototype.name(): return new ViewMode.DoNothing()
case ViewMode.Reduce.prototype.name(): return new ViewMode.Reduce()
case ViewMode.Hide.prototype.name(): return new ViewMode.Hide()
default: throw new Error(name)
}
}
ViewMode.prototype.isDoNothingMode = function() {
return this instanceof ViewMode.DoNothing
}
ViewMode.prototype.isHideMode = function() {
return this instanceof ViewMode.Hide
}
ViewMode.prototype.isReduceMode = function() {
return this instanceof ViewMode.Reduce
}
ViewMode.prototype.hasSameName = function(viewMode) {
return this.name() === viewMode.name()
}
ViewMode.DoNothing = function() {}
ViewMode.DoNothing.prototype = Object.create(ViewMode.prototype)
ViewMode.DoNothing.prototype.name = always('doNothing')
ViewMode.DoNothing.prototype.restoreViewMode = noop
ViewMode.DoNothing.prototype.setViewMode = noop
ViewMode.DoNothing.prototype.updateView = function(view) {
view.doNothingRadio.checked = true
}
ViewMode.Hide = function() {}
ViewMode.Hide.prototype = Object.create(ViewMode.prototype)
ViewMode.Hide.prototype.name = always('hide')
ViewMode.Hide.prototype.restoreViewMode = function(movieView) {
movieView.setVisible(true)
}
ViewMode.Hide.prototype.setViewMode = function(movieView) {
movieView.setVisible(false)
}
ViewMode.Hide.prototype.updateView = function(view) {
view.hideRadio.checked = true
}
ViewMode.Reduce = function() {}
ViewMode.Reduce.prototype = Object.create(ViewMode.prototype)
ViewMode.Reduce.prototype.name = always('reduce')
ViewMode.Reduce.prototype.restoreViewMode = function(movieView) {
var r = movieView.root
r.classList.remove('nrn-reduce')
this.restoreThumb(r.querySelector('.thumb'))
}
ViewMode.Reduce.prototype.setViewMode = function(movieView) {
var r = movieView.root
r.classList.add('nrn-reduce')
this.halfThumb(r.querySelector('.thumb'))
}
ViewMode.Reduce.prototype.updateView = function(view) {
view.reduceRadio.checked = true
}
ViewMode.Reduce.prototype.halfThumb = function(thumb) {
if (thumb.style.width) {
var w = this.srcThumbWidth = parseInt(thumb.style.width)
var t = this.srcThumbMarginTop = parseInt(thumb.style.marginTop)
thumb.style.width = parseInt(w / 2) + 'px'
thumb.style.marginTop = parseInt(t / 2) + 'px'
} else {
thumb.style.width = '100%'
}
}
ViewMode.Reduce.prototype.restoreThumb = function(thumb) {
if (this.srcThumbWidth) {
thumb.style.width = this.srcThumbWidth + 'px'
thumb.style.marginTop = this.srcThumbMarginTop + 'px'
} else {
thumb.style.width = ''
}
}
var MovieView = (function() {
var newParagraph = function(doc) {
return elem('p')
.attr('class', 'font12 nrn-movie-info-p')
.add([].slice.call(arguments, 1))
.new(doc)
}
var newActionButton = function(klass, doc) {
return elem('span').attr('class', klass).add('[+]').new(doc)
}
var OPEN_TOGGLE_TEXT = '開く▼'
var CLOSE_TOGGLE_TEXT = '閉じる▲'
var newMovieInfoToggleButton = function(doc) {
return elem('span')
.attr('class', 'count nrn-movie-info-toggle')
.add(elem('span')
.attr('class', 'value nrn-movie-info-toggle-button')
.add(OPEN_TOGGLE_TEXT)
.new(doc))
.new(doc)
}
var newDescriptionToggleButton = function(doc) {
return elem('span')
.attr('class', 'nrn-desc-toggle-button')
.add(OPEN_TOGGLE_TEXT)
.new(doc)
}
var newDescriptionToggleP = function(toggle, doc) {
return elem('p')
.attr('class', 'nrn-desc-toggle-p')
.add(toggle)
.new(doc)
}
var decodeHtmlCharRef = (function() {
var e = document.createElement('span')
return function(text) {
e.innerHTML = text
return e.textContent
}
})()
var newTagAnchor = function(tag, doc) {
return elem('a')
.attr({
href: 'http://www.nicovideo.jp/tag/' + tag,
target: '_blank',
class: 'nrn-movie-tag-link',
})
.add(decodeHtmlCharRef(tag))
.new(doc)
}
var newTagSpan = function(anchor, button) {
return elem('span')
.attr('class', 'nrn-movie-tag')
.add(anchor, button)
.new(anchor.ownerDocument)
}
var setTagActionAndStyle = function(tagView, ng) {
tagView.anchor.classList[ng ? 'add' : 'remove']('nrn-movie-ng-tag-link')
tagView.button.textContent = ng ? '[x]' : '[+]'
}
var newContributorA = function(doc) {
return elem('a')
.attr({'target': '_blank', 'class': 'nrn-contributor-link'})
.new(doc)
}
var Description = function(movieView) {
this.movieView = movieView
this.expanded = false
this.hasBeenSet = false
this.p = elem('p').attr('class', 'nrn-desc-p').new(movieView.view.doc)
this.toggle = newDescriptionToggleButton(movieView.view.doc)
this.toggleP = newDescriptionToggleP(this.toggle, movieView.view.doc)
}
Description.prototype.container = function() {
return this.movieView.root.querySelector('.itemDescription')
}
Description.prototype.isTruncated = function() {
return this.container().textContent.slice(-3) === '...'
}
Description.prototype.set = function(description) {
this.p.textContent = description
var e = this.container()
removeAllChild(e)
e.classList.add('nrn-desc')
e.appendChild(this.p)
this.hasBeenSet = true
}
Description.prototype.addToggle = function() {
this.container().appendChild(this.toggleP)
}
Description.prototype.removeToggle = function() {
removeFromParent(this.toggleP)
}
Description.prototype.setExpanded = function(expanded) {
this.expanded = expanded
this.p.classList[expanded ? 'add' : 'remove']('nrn-desc-p-close')
this.toggleP
.classList[expanded ? 'remove' : 'add']('nrn-desc-toggle-p-open')
this.toggle.textContent = expanded ? CLOSE_TOGGLE_TEXT : OPEN_TOGGLE_TEXT
}
Description.prototype.linkify = (function() {
var re = /(sm|so|nm|co|ar|im|lv|mylist\/|watch\/|user\/)(?:\d+)/g
var type2href = {
sm: 'http://www.nicovideo.jp/watch/',
so: 'http://www.nicovideo.jp/watch/',
nm: 'http://www.nicovideo.jp/watch/',
co: 'http://com.nicovideo.jp/community/',
ar: 'http://ch.nicovideo.jp/article/',
im: 'http://seiga.nicovideo.jp/seiga/',
lv: 'http://live.nicovideo.jp/watch/',
'mylist/': 'http://www.nicovideo.jp/',
'watch/': 'http://www.nicovideo.jp/',
'user/': 'http://www.nicovideo.jp/',
}
return function() {
var descElem = this.hasBeenSet ? this.p : this.container()
var text = descElem.textContent
var builder = elem()
var lastIndex = 0
for (var r; r = re.exec(text);) {
builder.add([
text.slice(lastIndex, r.index),
elem('a')
.attr({target: '_blank', href: type2href[r[1]] + r[0]})
.add(r[0])
.new(),
])
lastIndex = re.lastIndex
}
var df = builder.add(text.slice(lastIndex)).new()
df.normalize()
removeAllChild(descElem)
descElem.appendChild(df)
}
})()
var MovieInfo = function(movieView) {
this.movieView = movieView
this.visible = true
var d = movieView.view.doc
this.toggle = newMovieInfoToggleButton(d)
this.container = elem('div')
.attr('class', 'nrn-movie-info-container').new(d)
this.tagP = newParagraph(d)
this.contributorType = elem('span').add('ユーザー: ').new()
this.contributorA = newContributorA(d)
this.ngContributorButton =
newActionButton('nrn-contributor-ng-button', d)
this.contributorP = newParagraph(d
, this.contributorType
, this.contributorA
, this.ngContributorButton)
this.tag2view = Object.create(null)
}
MovieInfo.prototype.setParagraphsVisible = function(visible) {
;[this.tagP, this.contributorP].forEach(function(p) {
p.classList[visible ? 'remove' : 'add']('nrn-display-none')
})
}
MovieInfo.prototype.getItemDataElem = function() {
return this.movieView.root.querySelector('.itemData')
}
MovieInfo.prototype.updateVisible = function() {
var b = this.toggle
b.childNodes[0].textContent = this.visible ? CLOSE_TOGGLE_TEXT
: OPEN_TOGGLE_TEXT
this.setParagraphsVisible(this.visible)
}
MovieInfo.prototype.setVisible = function(visible) {
this.visible = visible
this.updateVisible()
}
MovieInfo.prototype.addToggle = function() {
var b = this.toggle
if (!b.parentNode) {
var e = this.getItemDataElem()
e.classList.add('nrn-movie-info-toggle-container')
e.appendChild(b)
}
this.setVisible(false)
}
MovieInfo.prototype.removeToggle = function() {
var b = this.toggle
if (b.parentNode) {
var e = this.getItemDataElem()
e.removeChild(b)
e.classList.remove('nrn-movie-info-toggle-container')
}
this.setVisible(true)
}
MovieInfo.prototype.getItemContent = function() {
return this.movieView.root.querySelector('.itemContent')
}
MovieInfo.prototype.addContainerToItemContent = function() {
var d = this.container
if (!d.parentNode) this.getItemContent().appendChild(d)
}
MovieInfo.prototype.setTags = function(tags) {
var d = this.movieView.view.doc
var t2v = this.tag2view
var p = this.tagP
tags.forEach(function(tag) {
var a = newTagAnchor(tag, d)
var b = newActionButton('nrn-tag-ng-button', d)
elem.add(p, newTagSpan(a, b), ' ')
t2v[tag] = { anchor: a, button: b }
})
this.container.appendChild(p)
this.addContainerToItemContent()
}
MovieInfo.prototype.updateByNgTags = function(ngTags) {
var t2v = this.tag2view
var tags = Object.keys(t2v)
var upperNgTags = ngTags.map(toUpperCase)
tags.filter(not(includeUpperCase(upperNgTags))).forEach(function(tag) {
setTagActionAndStyle(t2v[tag], false)
})
tags.filter(includeUpperCase(upperNgTags)).forEach(function(tag) {
setTagActionAndStyle(t2v[tag], true)
})
}
MovieInfo.prototype.setContributor = function(contributor) {
this.contributorType.textContent = contributor.isUser()
? 'ユーザー: ' : 'チャンネル: '
var preHref = contributor.isUser()
? 'http://www.nicovideo.jp/user/'
: 'http://ch.nicovideo.jp/channel/ch'
this.contributorA.href = preHref + contributor.id()
this.contributorA.textContent = contributor.name()
this.container.appendChild(this.contributorP)
this.addContainerToItemContent()
}
MovieInfo.prototype.updateContributorByNg = function(ng) {
var method = ng ? 'add' : 'remove'
this.contributorA.classList[method]('nrn-ng-contributor-link')
this.ngContributorButton.textContent = ng ? '[x]' : '[+]'
}
var Title = function(a) {
this.a = a
}
Title.prototype.update = function(movie) {
var a = this.a
if (!movie.isNgTitle()) {
a.textContent = a.textContent
return
}
removeAllChild(a)
var m = movie.matchedNgTitle()
var t = movie.title()
var i = t.toUpperCase().indexOf(m.toUpperCase())
if (i !== 0) elem.add(a, t.substring(0, i))
elem.add(a,
elem('span')
.attr('class', 'nrn-matched-ng-title')
.add(t.substring(i, i + m.length))
.new(a.ownerDocument))
if (i + m.length !== t.length) elem.add(a, t.substring(i + m.length))
}
Title.prototype.setLineThrough = function(lineThrough) {
this.a.classList[lineThrough ? 'add' : 'remove']('nrn-ng-movie-title')
}
Title.prototype.setTitle = function(title) {
this.a.textContent = title
}
var MovieView = function(view, titleAnchor, root) {
this.view = view
this.root = root
this.title = new Title(titleAnchor)
this.viewMode = new ViewMode.DoNothing()
this.movieInfo = this.newMovieInfo(this)
this.description = new Description(this)
this.errorElem = this.newErrorElem(view.doc)
}
MovieView.MovieInfo = MovieInfo
MovieView.prototype.newMovieInfo = function () {
return new MovieInfo(this)
}
MovieView.prototype.newErrorElem = function(doc) {
return elem('li').attr('class', 'count').css('color', 'red').new(doc)
}
MovieView.prototype.setVisible = function(visible) {
this.root.classList[visible ? 'remove' : 'add']('nrn-display-none')
}
MovieView.prototype.getMovieDataElem = function() {
return this.root.querySelector('ul.list')
}
MovieView.prototype.setViewModeIfDiff = function(viewMode) {
if (this.viewMode.hasSameName(viewMode)) return false
this.viewMode.restoreViewMode(this)
viewMode.setViewMode(this)
this.viewMode = viewMode
return true
}
MovieView.prototype.updateViewMode = function(movie) {
var m = this.view.model.movieViewMode(movie)
if (this.setViewModeIfDiff(m)) this.view.requestLoadingLazyImages()
}
MovieView.prototype.updateVisited = function(movie) {
this.updateViewMode(movie)
this.view.updatePopupMenu(movie)
}
MovieView.prototype.updateNgId = function(movie) {
this.updateViewMode(movie)
this.title.setLineThrough(movie.isNgId())
this.view.updatePopupMenu(movie)
}
MovieView.prototype.updateMatchedNgTitle = function(movie) {
this.title.update(movie)
}
MovieView.prototype.updateNgTitle = function(movie) {
this.updateViewMode(movie)
this.updateMatchedNgTitle(movie)
this.view.updatePopupMenu(movie)
}
MovieView.prototype._isMovieInfoTogglable = function () {
return this.view.model.movieInfoTogglable().get()
}
MovieView.prototype.updateTags = function(movie) {
this.movieInfo.setTags(movie.getTags())
if (this._isMovieInfoTogglable()) {
this.movieInfo.addToggle(movie)
}
}
MovieView.prototype.updateNgTags = function(movie) {
this.updateViewMode(movie)
this.movieInfo.updateByNgTags(movie.getNgTags())
}
MovieView.prototype.updateDescription = function(movie) {
var d = this.description
if (d.isTruncated()) {
d.set(movie.getDescription())
var togglable = this.view.model.descriptionTogglable().get()
d.setExpanded(!togglable)
if (togglable) d.addToggle()
}
d.linkify()
}
MovieView.prototype.updateError = function(movie) {
this.errorElem.textContent = movie.getError()
this.getMovieDataElem().appendChild(this.errorElem)
}
MovieView.prototype.updateContributor = function(movie) {
this.movieInfo.setContributor(movie.contributor())
if (this._isMovieInfoTogglable()) {
this.movieInfo.addToggle(movie)
}
}
MovieView.prototype.updateNgContributor = function(movie) {
this.updateViewMode(movie)
this.movieInfo.updateContributorByNg(movie.contributor().isNg())
}
MovieView.prototype.updateThumbInfo = function(movie) {
if (movie.getError()) {
this.updateError(movie)
} else {
this.updateTags(movie)
this.updateContributor(movie)
this.updateDescription(movie)
}
}
MovieView.prototype.updateTitle = function(movie) {
this.title.setTitle(movie.title())
}
return MovieView
})()
var LazyImageLoader = (function() {
var INTERVAL = 125
var LazyImageLoader = function(doc) {
this._doc = doc
this._imgs = null
this._requested = false
this._requestedInInterval = false
}
LazyImageLoader.prototype._lazyImages = function() {
if (this._imgs) {
return this._imgs.filter(function(img) {
return img.classList.contains('jsLazyImage')
})
}
return [].slice.call(this._doc.querySelectorAll('img.thumb.jsLazyImage'))
}
LazyImageLoader.prototype._lazyImagesInView = function() {
var divided = this._lazyImages().reduce((function(o, img) {
if (this._isInView(img)) o.inView.push(img)
else o.notInView.push(img)
return o
}).bind(this), {inView: [], notInView: []})
this._imgs = divided.notInView
return divided.inView
}
LazyImageLoader.prototype._isDone = function() {
return this._imgs && this._imgs.length === 0
}
LazyImageLoader.prototype._isInView = function(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)
}
LazyImageLoader.prototype._load = function() {
this._lazyImagesInView().forEach(function(img) {
img.src = img.dataset.original
img.dataset.original = ''
img.classList.remove('jsLazyImage')
})
}
LazyImageLoader.prototype._loadIfRequestedInInterval = function() {
if (this._requestedInInterval) {
this._requestedInInterval = false
this._load()
setTimeout(this._loadIfRequestedInInterval.bind(this), INTERVAL)
} else {
this._requested = false
}
}
LazyImageLoader.prototype.request = function() {
if (this._isDone()) return
if (this._requested) {
this._requestedInInterval = true
return
}
this._requested = true
setTimeout(this._load.bind(this))
setTimeout(this._loadIfRequestedInInterval.bind(this), INTERVAL)
}
return LazyImageLoader
})()
var GinzaView = (function() {
var radio = function(doc, value, clickCallback) {
return elem('input')
.attr({
'type': 'radio',
'name': 'visitedMovieViewMode',
'value': value,
})
.on('click', clickCallback)
.new(doc)
}
var movieIdInHRef = function(href) {
var execResult = /^watch\/([^?]+)/.exec(href)
return execResult ? execResult[1] : null
}
var bindClassContains = function(elem) {
return elem.classList.contains.bind(elem.classList)
}
var checkbox = function(doc, clickCallback, checked) {
return elem('input')
.attr({type: 'checkbox', checked: checked})
.on('click', clickCallback)
.new(doc)
}
var popupMenuButton = function(text, klass, doc) {
return elem('span').attr('class', klass).add(text).new(doc)
}
var movieNgButtonText = function(ng) {
return ng ? 'NG解除' : 'NG登録'
}
var newMovieNgButton = function(doc) {
return popupMenuButton(movieNgButtonText(false)
, 'nrn-movie-ng-button'
, doc)
}
var titleNgButtonText = function(ng) {
return ng ? 'NGタイトル削除' : 'NGタイトル追加'
}
var newTitleNgButton = function(doc) {
return popupMenuButton(titleNgButtonText(false)
, 'nrn-title-ng-button'
, doc)
}
var visitButtonText = function(ng) {
return ng ? '未閲覧' : '閲覧済み'
}
var newVisitButton = function(doc) {
return popupMenuButton(visitButtonText(false), 'nrn-visit-button', doc)
}
var newPopupMenu = function(doc) {
return elem('div')
.attr('class', 'nrn-popup')
.add([
newVisitButton(doc),
' | ',
newMovieNgButton(doc),
' | ',
newTitleNgButton(doc),
])
.new(doc)
}
var containsClass = curry(function(className, elem) {
return elem.classList.contains(className)
})
var GinzaView = function(doc) {
doc = doc || document
this.doc = doc
this.ngMovieVisibleCheckbox =
checkbox(doc, this._setNgMovieVisible.bind(this))
this.configButton = this._newConfigButton()
this.popup = newPopupMenu(doc)
this.idToMovieView = Object.create(null)
this.lazyImageLoader = new LazyImageLoader(doc)
var radioClickListener = this._setVisitedMovieViewMode.bind(this)
this.reduceRadio = radio(doc, 'reduce', radioClickListener)
this.hideRadio = radio(doc, 'hide', radioClickListener)
this.doNothingRadio = radio(doc, 'doNothing', radioClickListener)
}
GinzaView.prototype.setModel = function(model) {
this.model = model
this.updateVisitedMovieViewMode()
this.updateNewWindowOpen()
this.updateMovieInfoTogglable()
}
GinzaView.prototype._newConfigButton = function() {
return elem('span')
.attr('id', 'nrn-config-button')
.on('click', this.showConfigDialog.bind(this))
.add('設定')
.new(this.doc)
}
GinzaView.prototype._getMovieImages = function() {
return array(this.doc.querySelectorAll('img.thumb'))
}
GinzaView.prototype._getMovieAnchors = function() {
return this._getMovieImages()
.map(prop('parentNode'))
.concat(this._getWatchAnchors())
}
GinzaView.prototype._getWatchAnchors = function() {
return array(this.doc.querySelectorAll('p.itemTitle.ranking a'))
}
GinzaView.prototype._isMovieRoot = function(elem) {
return ['item', 'videoRanking'].every(bindClassContains(elem))
}
GinzaView.prototype._getMovieRoot = function(child) {
for (var e = child; e && e.classList; e = e.parentNode) {
if (this._isMovieRoot(e)) return e
}
return null
}
GinzaView.prototype._newMovieView = function(root) {
return new MovieView(this, root, this._getMovieRoot(root))
}
GinzaView.prototype.getMovies = function() {
var result = [], i2v = this.idToMovieView
this._getWatchAnchors().forEach(function(a) {
var id = movieIdInHRef(a.getAttribute('href'))
var movieView = i2v[id] = this._newMovieView(a)
result.push(new Movie(id, a.textContent, movieView))
}, this)
return result
}
GinzaView.prototype._newVisitedMovieViewModeFragment = function() {
return elem()
.add([
'閲覧済みの動画を',
elem('label').add(this.reduceRadio, ' 縮小').new(this.doc),
elem('label').add(this.hideRadio, ' 非表示').new(this.doc),
elem('label').add(this.doNothingRadio, ' 通常表示').new(this.doc),
])
.new(this.doc)
}
GinzaView.prototype._newControllers = function() {
return elem('div')
.add([
this._newVisitedMovieViewModeFragment(),
' | ',
elem('label').add(this.ngMovieVisibleCheckbox, ' NG動画を表示')
.new(this.doc),
' | ',
this.configButton,
])
.new(this.doc)
}
GinzaView.prototype.addControllers = function() {
var mainDiv = this.doc.querySelector('div.contentBody.video')
mainDiv.insertBefore(this._newControllers(), mainDiv.firstChild)
}
GinzaView.prototype.updateNewWindowOpen = function() {
var f = this.model.openNewWindow().get()
? callMethod('setAttribute', ['target', '_blank'])
: callMethod('removeAttribute', ['target'])
this._getMovieAnchors().forEach(f)
}
GinzaView.prototype.updateViewMode = function(movie) {
this.idToMovieView[movie.id()].updateViewMode(movie)
}
GinzaView.prototype.updateVisitedMovieViewMode = function() {
this.model.visitedMovieViewMode().get().updateView(this)
this.model.movies().forEach(this.updateViewMode, this)
}
GinzaView.prototype.updateNgMovieVisible = function() {
this.ngMovieVisibleCheckbox.checked = this.model.isNgMovieVisible()
this.model.movies().forEach(this.updateViewMode, this)
}
GinzaView.prototype.toggleDescriptionExpanded = function(event) {
var movieId = this.movieIdByComponent(event.target)
var d = this.idToMovieView[movieId].description
d.setExpanded(!d.expanded)
}
GinzaView.prototype.updateDescriptionTogglable = function() {
var togglable = this.model.descriptionTogglable().get()
var methodName = togglable ? 'addToggle' : 'removeToggle'
var id2view = this.idToMovieView
Object.keys(id2view)
.map(flip(prop)(id2view))
.filter(compose(prop('hasBeenSet'), prop('description')))
.forEach(function(view) {
view.description[methodName]()
view.description.setExpanded(!togglable)
})
}
GinzaView.prototype._addMovieInfoToggleButton = function(movie) {
this.idToMovieView[movie.id()].movieInfo.addToggle()
}
GinzaView.prototype._removeMovieInfoToggleButton = function(movie) {
this.idToMovieView[movie.id()].movieInfo.removeToggle()
}
GinzaView.prototype.toggleMovieInfoVisible = function(event) {
var movieId = this.movieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.movieInfo.setVisible(!v.movieInfo.visible)
}
GinzaView.prototype.updateMovieInfoTogglable = function() {
var f = this.model.movieInfoTogglable().get()
? this._addMovieInfoToggleButton
: this._removeMovieInfoToggleButton
this.model.movies()
.filter(callMethod('hasMovieInfo', []))
.forEach(f, this)
}
GinzaView.prototype._setVisitedMovieViewMode = function() {
if (this.reduceRadio.checked)
this.model.visitedMovieViewMode().set(new ViewMode.Reduce())
else if (this.hideRadio.checked)
this.model.visitedMovieViewMode().set(new ViewMode.Hide())
else if (this.doNothingRadio.checked)
this.model.visitedMovieViewMode().set(new ViewMode.DoNothing())
else
throw new Error()
}
GinzaView.prototype._setNgMovieVisible = function() {
this.model.setNgMovieVisible(this.ngMovieVisibleCheckbox.checked)
}
GinzaView.prototype.showConfigDialog = function() {
ConfigDialog.show(this.model)
}
GinzaView.prototype.requestLoadingLazyImages = function() {
this.lazyImageLoader.request()
}
GinzaView.prototype.movieIdByComponent = function(elem) {
var r = this._getMovieRoot(elem)
return r ? r.dataset.id : ''
}
GinzaView.prototype.movieIdByPopup = function() {
return this.movieIdByComponent(this.popup)
}
GinzaView.prototype.tagByTagNgButton = function(tagNgButton) {
return tagNgButton.previousSibling.textContent
}
GinzaView.prototype.isMovieNgButton = containsClass('nrn-movie-ng-button')
GinzaView.prototype.isTitleNgButton = containsClass('nrn-title-ng-button')
GinzaView.prototype.isVisitButton = containsClass('nrn-visit-button')
GinzaView.prototype.isTagNgButton = containsClass('nrn-tag-ng-button')
GinzaView.prototype.isContributorNgButton =
containsClass('nrn-contributor-ng-button')
GinzaView.prototype.isTitleAnchor = function(elem) {
var a = ancestorAnchor(elem)
return Boolean(a)
&& Boolean(a.parentNode)
&& ['itemTitle', 'ranking'].every(bindClassContains(a.parentNode))
}
GinzaView.prototype.isThumbAnchor = function(elem) {
var a = ancestorAnchor(elem)
return Boolean(a) && a.matches(
'.videoList01Wrap > .itemThumbBox > .itemThumb > a.itemThumbWrap')
}
GinzaView.prototype.isMovieInfoToggle =
containsClass('nrn-movie-info-toggle-button')
GinzaView.prototype.isDescriptionToggle =
containsClass('nrn-desc-toggle-button')
GinzaView.prototype.contentBody = function() {
return this.doc.querySelector('div.contentBody.video.videoList01')
}
GinzaView.prototype.updatePopupMenu = function(movie) {
var p = this.popup
if (!p.parentNode) return
if (this.movieIdByComponent(p) !== movie.id()) return
p.childNodes[0].textContent = visitButtonText(movie.isVisited())
p.childNodes[2].textContent = movieNgButtonText(movie.isNgId())
p.childNodes[4].textContent = titleNgButtonText(movie.isNgTitle())
}
GinzaView.prototype.addPopupMenuTo = function(elem) {
this._getMovieRoot(elem).appendChild(this.popup)
this.updatePopupMenu(this.model.movieById(this.movieIdByComponent(elem)))
}
GinzaView.prototype.removePopupMenu = function() {
removeFromParent(this.popup)
}
GinzaView.prototype._getRankingNumCounterCssText = function () {
var e = this.doc.querySelector('.rankingNum')
var n = e ? parseInt(e.textContent) - 1 : 0
return `body {
counter-increment: ranking ${n};
}
.video .item.videoRanking {
counter-increment: ranking;
}
.videoList01 .item.videoRanking .rankingNumWrap .rankingNum {
font-size: 0;
}
.videoList01 .item.videoRanking .rankingNumWrap .rankingNum::after {
content: counter(ranking, decimal);
font-size: 40px;
line-height: 1.3;
}`
}
GinzaView.prototype.getCssText = function () {
return commonCssText() + '\n' + [
'.item.videoRanking:hover .nrn-popup {',
' display: inherit;',
'}',
'.nrn-reduce .rankingPt,',
'.nrn-reduce .itemTime,',
'.nrn-reduce .wrap,',
'.nrn-reduce .itemData,',
'.nrn-reduce .nrn-movie-info-container {',
' display: none;',
'}',
'.nrn-reduce .rankingNum {',
' font-size: 150%;',
'}',
'.nrn-reduce .videoList01Wrap {',
' width: 80px;',
'}',
'.videoList01 .nrn-reduce .itemContent .itemTitle.ranking {',
' width: auto;',
'}',
'.video .nrn-reduce .itemThumbBox,',
'.video .nrn-reduce .itemThumbBox .itemThumb,',
'.video .nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap {',
' width: 80px;',
' height: 45px;',
'}',
'.nrn-contributor-link {',
' color: rgb(51, 51, 51);',
'}',
'.nrn-ng-contributor-link {',
' color: white;',
' background-color: fuchsia;',
'}',
'.nrn-movie-tag-link {',
' color: rgb(51, 51, 51);',
'}',
'.nrn-movie-ng-tag-link {',
' color: white;',
' background-color: fuchsia;',
'}',
'.nrn-desc-toggle-button {',
' cursor: pointer;',
' text-decoration: underline;',
'}',
'.nrn-desc-toggle-p {',
' text-align: right;',
'}',
'.nrn-desc-toggle-p-open {',
' position: absolute;',
' top: 0;',
' right: 0;',
'}',
'.nrn-desc-p {',
' width: 400px;',
' height: 14px;',
'}',
'.nrn-desc-p.nrn-desc-p-close {',
' width: 440px;',
' height: auto;',
'}',
'.videoList01 .itemContent .itemDescription.ranking.nrn-desc {',
' height: auto;',
' position: relative;',
'}',
'.nrn-movie-info-toggle {',
' position: absolute;',
' top: 0;',
' right: 0;',
'}',
'.video .itemData .count .value.nrn-movie-info-toggle-button {',
' cursor: pointer;',
' text-decoration: underline;',
' padding: 0;',
'}',
'.nrn-movie-info-toggle-container {',
' position: relative;',
' width: 440px;',
'}',
'.nrn-movie-tag {',
' white-space: nowrap;',
' margin-right: 0.5em;',
'}',
'.nrn-ng-movie-title {',
' text-decoration: line-through;',
'}',
this._getRankingNumCounterCssText(),
].join('\n')
}
return GinzaView
})()
var MatrixView = (function (_super) {
var MatrixView = function () {
_super.apply(this, arguments)
this._relocationCellsRequested = false
}
MatrixView.is = function (location) {
return location.pathname.startsWith('/ranking/matrix')
}
MatrixView.prototype = Object.create(_super.prototype)
MatrixView.prototype.contentBody = function() {
return this.doc.querySelector('.column.main')
}
MatrixView.prototype._getWatchAnchors = function() {
return array(this.doc.querySelectorAll('.itemTitle > a'))
}
MatrixView.prototype._isMovieRoot = function(elem) {
return elem.tagName === 'TD'
}
MatrixView.prototype._newMovieView = function(root) {
return new MatrixMovieView(this, root, this._getMovieRoot(root))
}
MatrixView.prototype.addControllers = function() {
var c = this._newControllers()
c.style.marginTop = '10px'
var b = this.contentBody()
b.insertBefore(c, b.firstChild)
}
MatrixView.prototype.isTitleAnchor = function(elem) {
var a = ancestorAnchor(elem)
return Boolean(a)
&& Boolean(a.parentNode)
&& a.parentNode.classList.contains('itemTitle')
}
MatrixView.prototype.isThumbAnchor = function(elem) {
var a = ancestorAnchor(elem)
return Boolean(a) && a.classList.contains('itemThumbWrap')
}
MatrixView.prototype.getCssText = function () {
return commonCssText() + '\n' + [
'td:hover .nrn-popup {',
' display: inherit;',
' z-index: 9999;',
'}',
'.top20 .bg_grade_0 a.nrn-contributor-link:link {',
' color: black !important;',
'}',
'.top20 .bg_grade_0 a.nrn-contributor-link:link:hover {',
' background-color: initial;',
'}',
'.top20 .bg_grade_0 a.nrn-ng-contributor-link:link {',
' color: white !important;',
' background-color: fuchsia;',
'}',
'.top20 .bg_grade_0 a.nrn-movie-tag-link:link {',
' color: black !important;',
'}',
'.top20 .bg_grade_0 a.nrn-movie-tag-link:link:hover {',
' background-color: initial;',
'}',
'.top20 .bg_grade_0 a.nrn-movie-ng-tag-link:link {',
' color: white !important;',
' background-color: fuchsia;',
'}',
'.nrn-movie-info-toggle-button {',
' cursor: pointer;',
' text-decoration: underline;',
'}',
'td[data-video-item] {',
' position: relative;',
'}',
'table.top20 tr td .nrn-movie-info-toggle-container {',
' background-position: top;',
'}',
'.nrn-movie-info-toggle {',
' display: block;',
' text-align: right;',
' margin-top: 5px;',
'}',
'.nrn-movie-info-container .nrn-movie-info-p {',
' text-align: left;',
'}',
'.nrn-movie-tag {',
' display: block;',
'}',
'.nrn-tag-ng-button,',
'.nrn-contributor-ng-button {',
' white-space: nowrap;',
'}',
'.nrn-reduce .itemThumbBox,',
'.nrn-reduce .itemThumbBox .itemThumb {',
' width: 50px;',
' height: 38px;',
' margin: 0;',
'}',
'.top20 .itemTitle a.nrn-ng-movie-title {',
' text-decoration: line-through;',
'}',
'.nrn-hidden {',
' visibility: hidden;',
'}',
'.nrn-hidden > .nrn-movie-info-container,',
'.nrn-hidden > .nrn-movie-info-toggle {',
' display: none;',
'}',
].join('\n')
}
MatrixView.prototype.requestLoadingLazyImages = noop
MatrixView.prototype.updateMovieInfoTogglable = noop
return MatrixView
})(GinzaView)
var MatrixMovieView = (function(_super) {
var MatrixMovieInfo = function(movieView) {
_super.MovieInfo.call(this, movieView)
}
MatrixMovieInfo.prototype = Object.create(_super.MovieInfo.prototype)
MatrixMovieInfo.prototype.getItemDataElem = function() {
return this.movieView.root
}
MatrixMovieInfo.prototype.getItemContent = function() {
return this.movieView.root
}
var MatrixMovieView = function(view, titleAnchor, root) {
_super.call(this, view, titleAnchor, root)
}
MatrixMovieView.MatrixMovieInfo = MatrixMovieInfo
MatrixMovieView.prototype = Object.create(_super.prototype)
MatrixMovieView.prototype.newMovieInfo = function () {
return new MatrixMovieInfo(this)
}
MatrixMovieView.prototype.newErrorElem = function(doc) {
return elem('div').css('color', 'red').new(doc)
}
MatrixMovieView.prototype.setVisible = function(visible) {
this.root.classList[visible ? 'remove' : 'add']('nrn-hidden')
}
MatrixMovieView.prototype.getMovieDataElem = function() {
return this.root
}
MatrixMovieView.prototype._isMovieInfoTogglable = always(true)
MatrixMovieView.prototype.updateDescription = noop
return MatrixMovieView
})(MovieView)
var ConfigDialog = (function() {
var opt = function(val, text, selected) {
return elem('option').attr({value: val, selected: selected}).add(text)
}
var action = function(targetOption) {
var noEmptyStr = function(v) { return v !== '' }
var isPositiveInt = function(v) {
var i = parseInt(v)
return !isNaN(i) && i > 0
}
var toInt = function(v) { return parseInt(v) }
var errMessage = '1以上の整数を入力して下さい。\n'
return {
'ng-movie-id': {
get: function(model) { return model.ngMovieIds().get() },
add: function(model, val) { return model.ngMovieIds().add(val) },
remove: function(model, vals) { model.ngMovieIds().remove(vals) },
removeAll: function(model) { model.ngMovieIds().clear() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/watch/' + v },
},
'ng-title': {
get: function(model) { return model.ngTitles().get() },
add: function(model, val) { return model.ngTitles().add(val) },
remove: function(model, vals) { model.ngTitles().remove(vals) },
removeAll: function(model) { model.ngTitles().clear() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/search/' + v },
},
'ng-tag': {
get: function(model) { return model.ngTags().get() },
add: function(model, val) { return model.ngTags().add(val) },
remove: function(model, vals) { model.ngTags().remove(vals) },
removeAll: function(model) { model.ngTags().clear() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/tag/' + v },
},
'ng-user-id': {
get: function(model) { return model.ngUserIds().get() },
add: function(model, val) {
return model.ngUserIds().add(parseInt(val))
},
remove: function(model, vals) {
model.ngUserIds().remove(vals.map(toInt))
},
removeAll: function(model) { model.ngUserIds().clear() },
valid: isPositiveInt,
errMessage: errMessage,
url: function(v) { return 'http://www.nicovideo.jp/user/' + v },
},
'ng-channel-id': {
get: function(model) { return model.ngChannelIds().get() },
add: function(model, val) {
return model.ngChannelIds().add(parseInt(val))
},
remove: function(model, vals) {
model.ngChannelIds().remove(vals.map(toInt))
},
removeAll: function(model) { model.ngChannelIds().clear() },
valid: isPositiveInt,
errMessage: errMessage,
url: function(v) { return 'http://ch.nicovideo.jp/ch' + v },
},
'visited-movie-id': {
get: function(model) { return model.visitedMovieIds().get() },
add: function(model, val) {
return model.visitedMovieIds().add(val)
},
remove: function(model, vals) {
model.visitedMovieIds().remove(vals)
},
removeAll: function(model) { model.visitedMovieIds().clear() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/watch/' + v },
},
}[targetOption.value]
}
var buttonStyle = {
display: 'block',
width: '100%',
'margin-bottom': '5px',
}
var setupDoc = function(doc) {
elem.css(doc.body, { width: '20em', margin: '8px' })
return doc
}
var newBackground = function(zIndex) {
return elem('div')
.css({
'background-color': 'black',
opacity: '0.5',
'z-index': String(zIndex),
position: 'fixed',
top: '0px',
left: '0px',
width: '100%',
height: '100%',
})
.new()
}
var checkbox = function(name, storeName) {
var checked = storeName ? this.model[storeName]().get()
: this.model['is' + name]()
return elem('input')
.attr({ type: 'checkbox', checked: checked })
.on('change', this['update' + name].bind(this))
.new(this.doc)
}
var label = function(control, text) {
return elem('label').css('display', 'block').add(control, text)
}
var ConfigDialog = function(model, doc) {
this.model = model
this.doc = setupDoc(doc)
this.background = newBackground(ConfigDialog.Z_INDEX - 1)
this.iframe = null
this.target = this.newTarget()
this.list = this.newList()
this.addButton = this.newAddButton()
this.removeButton = this.newRemoveButton()
this.removeAllButton = this.newRemoveAllButton()
this.openButton = this.newOpenButton()
this.newWindowOpenCheckbox =
checkbox.call(this, 'NewWindowOpen', 'openNewWindow')
this.useGetThumbInfoCheckbox =
checkbox.call(this, 'UseGetThumbInfo', 'useGetThumbInfo')
this.movieInfoTogglableCheckbox =
checkbox.call(this, 'MovieInfoTogglable', 'movieInfoTogglable')
this.descriptionTogglableCheckbox =
checkbox.call(this, 'DescriptionTogglable', 'descriptionTogglable')
this.closeButton = this.newCloseButton()
this.doc.body.appendChild(this.newRoot())
this.updateButtonDisabled()
}
ConfigDialog.Z_INDEX = 10000
ConfigDialog.show = function(model, callback) {
var f = document.createElement('iframe')
f.addEventListener('load', function() {
var d = new ConfigDialog(model, f.contentDocument)
d.setup(f)
if (callback) callback(d)
})
document.body.appendChild(f)
}
ConfigDialog.prototype.newTarget = function() {
return elem('select')
.css('width', '100%')
.add([
opt('ng-movie-id', 'NG動画ID').new(this.doc),
opt('ng-title', 'NGタイトル', true).new(this.doc),
opt('ng-tag', 'NGタグ').new(this.doc),
opt('ng-user-id', 'NGユーザーID').new(this.doc),
opt('ng-channel-id', 'NGチャンネルID').new(this.doc),
opt('visited-movie-id', '閲覧済み動画ID').new(this.doc),
])
.on('change', this.updateList.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newList = function() {
return elem('select')
.attr({ size: 10, multiple: true })
.css('width', '100%')
.add(this.model.ngTitles().get().map(function(t) {
return opt(t, t).new(this.doc)
}, this))
.on('change', this.updateButtonDisabled.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newAddButton = function() {
return elem('button')
.css(buttonStyle)
.add('追加')
.on('click', this.addPromptResult.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newRemoveButton = function() {
return elem('button')
.css(buttonStyle)
.add('削除')
.on('click', this.removeSelectedItems.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newRemoveAllButton = function() {
return elem('button')
.css(buttonStyle)
.add('全削除')
.on('click', this.removeAll.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newOpenButton = function() {
return elem('button')
.css(buttonStyle)
.add('開く')
.on('click', this.openSelectedItems.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newCloseButton = function() {
return elem('button')
.css({ display: 'block', margin: '10px auto 0px' })
.add('閉じる')
.on('click', this.remove.bind(this))
.new(this.doc)
}
ConfigDialog.prototype.newRoot = function() {
return elem()
.add([
elem('div').css('width', '15em').add(this.target).new(this.doc),
elem('div')
.css({ width: '15em', float: 'left' })
.add(this.list)
.new(this.doc),
elem('div')
.css({ width: '5em', float: 'left' })
.add([
this.addButton,
this.removeButton,
this.removeAllButton,
this.openButton,
])
.new(this.doc),
label(this.newWindowOpenCheckbox, '動画を別窓で開く')
.css({ clear: 'both', 'padding-top': '10px '})
.new(this.doc),
label(this.useGetThumbInfoCheckbox, '動画情報を取得する')
.new(this.doc),
elem('fieldset')
.add([
elem('legend').add('表示・非表示の切り替えボタン').new(this.doc),
label(this.movieInfoTogglableCheckbox, 'タグ、ユーザー、チャンネル')
.new(this.doc),
label(this.descriptionTogglableCheckbox, '動画説明')
.new(this.doc),
])
.new(this.doc),
this.closeButton,
])
.new(this.doc)
}
ConfigDialog.prototype.setup = function(iframe) {
document.body.appendChild(this.background)
this.iframe = elem.css(iframe, {
'background-color': 'white',
position: 'fixed',
border: 'medium solid black',
'z-index': ConfigDialog.Z_INDEX,
})
this.size(iframe)
this.center(iframe)
}
ConfigDialog.prototype.size = function(iframe) {
var s = this.doc.body.style
iframe.width = this.doc.body.offsetWidth
+ parseInt(s.marginLeft)
+ parseInt(s.marginRight)
iframe.height = this.doc.documentElement.offsetHeight
}
ConfigDialog.prototype.center = function(iframe) {
iframe.style.left = ((window.innerWidth - iframe.width) / 2) + 'px'
iframe.style.top = ((window.innerHeight - iframe.height) / 2) + 'px'
}
ConfigDialog.prototype.remove = function() {
var f = this.iframe
if (f && f.parentNode) f.parentNode.removeChild(f)
var b = this.background
if (b.parentNode) b.parentNode.removeChild(b)
}
ConfigDialog.prototype.selectedTargetOption = function() {
return this.target.options[this.target.selectedIndex]
}
ConfigDialog.prototype.addPromptResult = function() {
var o = this.selectedTargetOption()
var a = action(o)
var r = null
do {
r = window.prompt((r ? '"' + r + '"は登録済みです。\n' : '') + o.text
, r || '')
if (r === null) return
while (!a.valid(r)) {
r = window.prompt(a.errMessage + o.text, r)
if (r === null) return
}
} while (!a.add(this.model, r))
this.list.appendChild(opt(r, r).new(this.doc))
this.updateButtonDisabled()
}
ConfigDialog.prototype.removeSelectedItems = function() {
var opts = [].slice.call(this.list.selectedOptions)
opts.forEach(function(o) { o.parentNode.removeChild(o) })
var selected = this.selectedTargetOption()
action(selected).remove(this.model
, opts.map(function(o) { return o.value }))
this.updateButtonDisabled()
}
ConfigDialog.prototype.removeAll = function() {
var o = this.selectedTargetOption()
if (!window.confirm('すべての"' + o.text + '"を削除しますか?')) return
action(o).removeAll(this.model)
;[].slice.call(this.list.options).forEach(function(o) {
o.parentNode.removeChild(o)
})
this.updateButtonDisabled()
}
ConfigDialog.prototype.openSelectedItems = function() {
var a = action(this.selectedTargetOption())
;[].forEach.call(this.list.selectedOptions, function(o) {
GM_openInTab(a.url(o.value))
})
}
ConfigDialog.prototype.updateList = function() {
;[].slice.call(this.list.options).forEach(function(o) {
o.parentNode.removeChild(o)
})
var o = this.selectedTargetOption()
action(o).get(this.model).forEach(function(v) {
this.list.appendChild(opt(v, v).new(this.doc))
}, this)
this.updateButtonDisabled()
}
ConfigDialog.prototype.updateButtonDisabled = function() {
this.removeAllButton.disabled = !this.list.options.length
var disabled = this.list.selectedIndex === -1
this.removeButton.disabled = disabled
this.openButton.disabled = disabled
}
ConfigDialog.prototype.updateNewWindowOpen = function() {
this.model.openNewWindow().set(this.newWindowOpenCheckbox.checked)
}
ConfigDialog.prototype.updateUseGetThumbInfo = function() {
this.model.useGetThumbInfo().set(this.useGetThumbInfoCheckbox.checked)
}
ConfigDialog.prototype.updateMovieInfoTogglable = function() {
this.model.movieInfoTogglable()
.set(this.movieInfoTogglableCheckbox.checked)
}
ConfigDialog.prototype.updateDescriptionTogglable = function() {
this.model.descriptionTogglable()
.set(this.descriptionTogglableCheckbox.checked)
}
return ConfigDialog
})()
var Controller = (function() {
var movieByTarget = function(fn) {
return function(target) {
var movieId = this._view.movieIdByComponent(target)
fn.call(this, this._model.movieById(movieId), target)
}
}
var method = function(b) {
return b ? 'remove' : 'add'
}
var Controller = function(model, view) {
this._model = model
this._view = view
}
Controller.prototype._addVisitedId = function(target) {
this._model.visitedMovieIds().add(this._view.movieIdByComponent(target))
}
Controller.prototype._changeNgIds = movieByTarget(function(movie) {
this._model.ngMovieIds()[method(movie.isNgId())](movie.id())
})
Controller.prototype._promptUntilAdd = function(movie) {
var r = null
do {
var msg = (r ? '"' + r + '"は登録済みです。\n' : '') + 'NGタイトルを入力'
r = prompt(msg, r ? r : movie.title())
} while (r && !this._model.ngTitles().add(r))
}
Controller.prototype._changeNgTitles = movieByTarget(function(movie) {
if (movie.isNgTitle()) {
this._model.ngTitles().remove(movie.matchedNgTitle())
} else {
this._promptUntilAdd(movie)
}
})
Controller.prototype._changeVisistedIds = movieByTarget(function(movie) {
this._model.visitedMovieIds()[method(movie.isVisited())](movie.id())
})
Controller.prototype._changeNgTags = movieByTarget(function(movie, target) {
var tag = this._view.tagByTagNgButton(target)
this._model.ngTags()[method(movie.hasNgTag(tag))](tag)
})
Controller.prototype._changeNgContributors = movieByTarget(function(movie) {
var methodName = method(movie.contributor().isNg())
var storeName = movie.contributor().isUser()
? 'ngUserIds' : 'ngChannelIds'
this._model[storeName]()[methodName](movie.contributor().id())
})
Controller.prototype.clickCallback = function(event) {
var t = event.target
if (this._view.isThumbAnchor(t) || this._view.isTitleAnchor(t)) {
this._addVisitedId(t)
} else if (this._view.isMovieNgButton(t)) {
this._changeNgIds(t)
} else if (this._view.isTitleNgButton(t)) {
this._changeNgTitles(t)
} else if (this._view.isVisitButton(t)) {
this._changeVisistedIds(t)
} else if (this._view.isTagNgButton(t)) {
this._changeNgTags(t)
} else if (this._view.isContributorNgButton(t)) {
this._changeNgContributors(t)
} else if (this._view.isMovieInfoToggle(t)) {
this._view.toggleMovieInfoVisible(event)
} else if (this._view.isDescriptionToggle(t)) {
this._view.toggleDescriptionExpanded(event)
}
}
Controller.prototype.mouseOverCallback = function(event) {
var targetId = this._view.movieIdByComponent(event.target)
var popupId = this._view.movieIdByPopup()
if (targetId === popupId) return
if (targetId) {
this._view.addPopupMenuTo(event.target)
} else {
this._view.removePopupMenu()
}
}
return Controller
})()
var main = function() {
var view = MatrixView.is(window.location)
? new MatrixView() : new GinzaView()
GM_addStyle(view.getCssText())
var movies = view.getMovies()
var stores = newStores()
var model = new Model(movies, view, stores)
view.setModel(model)
view.addControllers()
updateMovies(movies, stores)
var body = view.contentBody()
var ctrl = new Controller(model, view)
body.addEventListener('click', ctrl.clickCallback.bind(ctrl))
body.addEventListener('mouseover', ctrl.mouseOverCallback.bind(ctrl))
window.addEventListener('scroll', view.requestLoadingLazyImages.bind(view))
if (stores.useGetThumbInfo.get()) {
new GetThumbInfo(GM_xmlhttpRequest
, model.sortMoviesByVisible()
, stores
, 5).request()
}
}
main()
})()