// ==UserScript==
// @name Nico Nico Ranking NG
// @namespace http://userscripts.org/users/121129
// @description ニコニコ動画のランキングにNG機能を追加
// @match http://www.nicovideo.jp/ranking*
// @version 18
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_addStyle
// @license MIT License
// @noframes
// @run-at document-start
// ==/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')),
visibleContributorType: new Store(o('visibleContributorType', 'all')),
openNewWindow: new Store(o('openNewWindow', true)),
useGetThumbInfo: new Store(o('useGetThumbInfo', true)),
movieInfoTogglable: new Store(o('movieInfoTogglable', true)),
seamlessRankingNumber: new Store(o('seamlessRankingNumber', true)),
descriptionTogglable: new Store(o('descriptionTogglable', true)),
requestingNext: new Store(o('requestingNext', true)),
popupVisible: new Store(o('popupVisible', true)),
movingUp: new Store(o('movingUp', 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.isNull = function() {
return this === Contributor.null
}
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)
}
Movie.prototype.setGetThumbInfoDone = function() {
this._view.updateGetThumbInfoDone(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.slice()
this._concurrent = concurrent
this._stores = stores
this._requestCount = 0
}
GetThumbInfo.prototype._onerror = function(movie, message) {
this._requestCount--
this._requestMovie()
movie.setThumbInfo({error: message})
movie.setGetThumbInfoDone()
}
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(movie, res) {
this._requestCount--
this._requestMovie()
if (res.status === 200) {
var thumbInfo = parseResText(res.responseText)
movie.setThumbInfo(thumbInfo)
if (!thumbInfo.error) this._setNg(movie)
} else {
movie.setThumbInfo({error: res.statusText})
}
movie.setGetThumbInfoDone()
}
GetThumbInfo.prototype._requestMovie = function() {
var m = this._movies.shift()
if (!m) return
this._request({
method: 'GET',
url: 'http://ext.nicovideo.jp/api/getthumbinfo/' + m.id(),
timeout: 5000,
onload: this._onload.bind(this, m),
onerror: this._onerror.bind(this, m, 'エラー'),
ontimeout: this._onerror.bind(this, m, 'タイムアウト'),
})
this._requestCount++
}
GetThumbInfo.prototype.request = function(movies) {
;[].push.apply(this._movies, movies || [])
var space = this._concurrent - this._requestCount
var c = Math.min(this._movies.length, space)
for (var i = 0; i < c; i++) this._requestMovie()
}
return GetThumbInfo
})()
var Model = (function() {
var movieUpdater = function(model, setter) {
var flippedSetterCall = flip(setter.call.bind(setter))
return function(store) {
model._movies.forEach(flippedSetterCall(store.get()))
}
}
var Model = function(movies, view, stores) {
this._movies = movies
this._view = view
this._stores = stores
this._ngMovieVisible = false
this._useGetThumbInfo = stores.useGetThumbInfo
this._requestingNext = stores.requestingNext
this._popupVisible = stores.popupVisible
this._visitedMovieViewMode =
new ViewModeStore(
new ObservableStore(stores.visitedMovieViewMode
, view.updateVisitedMovieViewMode.bind(view)))
this._visibleContributorType =
new ObservableStore(stores.visibleContributorType
, view.updateVisibleContributorType.bind(view))
this._openNewWindow =
new ObservableStore(stores.openNewWindow
, view.updateNewWindowOpen.bind(view))
this._seamlessRankingNumber =
new ObservableStore(stores.seamlessRankingNumber
, view.updateSeamlessRankingNumber.bind(view))
this._movieInfoTogglable =
new ObservableStore(stores.movieInfoTogglable
, view.updateMovieInfoTogglable.bind(view))
this._descriptionTogglable =
new ObservableStore(stores.descriptionTogglable
, view.updateDescriptionTogglable.bind(view))
this._movingUp =
new ObservableStore(stores.movingUp, view.updateMovingUp.bind(view))
var p = Movie.prototype
this._visitedMovieIds =
new ObservableStore(stores.visitedMovieIds
, movieUpdater(this, p.setVisitedIfInclude))
this._ngMovieIds =
new ObservableStore(stores.ngMovieIds
, movieUpdater(this, p.setNgIdIfInclude))
this._ngTitles =
new ObservableStore(stores.ngTitles
, movieUpdater(this, p.setNgTitleIfInclude))
this._ngTags =
new ObservableStore(stores.ngTags
, movieUpdater(this, p.setNgTagsIfMatch))
this._ngUserIds =
new ObservableStore(stores.ngUserIds
, movieUpdater(this, p.setNgUserIfInclude))
this._ngChannelIds =
new ObservableStore(stores.ngChannelIds
, movieUpdater(this, p.setNgChannelIfInclude))
}
Model.prototype.movies = getter('_movies')
Model.prototype.stores = getter('_stores')
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.visibleContributorType = getter('_visibleContributorType')
Model.prototype.openNewWindow = getter('_openNewWindow')
Model.prototype.useGetThumbInfo = getter('_useGetThumbInfo')
Model.prototype.requestingNext = getter('_requestingNext')
Model.prototype.popupVisible = getter('_popupVisible')
Model.prototype.seamlessRankingNumber = getter('_seamlessRankingNumber')
Model.prototype.movieInfoTogglable = getter('_movieInfoTogglable')
Model.prototype.descriptionTogglable = getter('_descriptionTogglable')
Model.prototype.movingUp = getter('_movingUp')
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._isHiddenByVisitedHideMode = function(movie) {
return movie.isVisited()
&& this.visitedMovieViewMode().get().isHideMode()
}
Model.prototype._isHiddenByContributorType = function(movie) {
if (movie.contributor().isNull()) return false
var t = this.visibleContributorType().get()
return (t === 'channel' && !movie.contributor().isChannel())
|| (t === 'user' && !movie.contributor().isUser())
}
Model.prototype._isHidden = function(movie) {
return !this._isVisible(movie)
|| this._isHiddenByVisitedHideMode(movie)
|| this._isHiddenByContributorType(movie)
}
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(movies) {
var self = this
return (movies || 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'))
}
Model.prototype.addMovies = function (movies) {
this._movies = this._movies.concat(movies)
}
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.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.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.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)
this.updateViewMode(movie)
}
}
MovieView.prototype.updateTitle = function(movie) {
this.title.setTitle(movie.title())
}
MovieView.prototype.updateGetThumbInfoDone = function() {
this.root.classList.add('nrn-getThumbInfoDone')
}
return MovieView
})()
var LazyImageLoader = (function() {
var INTERVAL = 125
var LazyImageLoader = function(doc) {
this._doc = doc
this._requested = false
this._requestedInInterval = false
}
LazyImageLoader.prototype._lazyImages = function() {
return [].slice.call(this._doc.querySelectorAll('img.thumb.jsLazyImage'))
}
LazyImageLoader.prototype._lazyImagesInView = function() {
return this._lazyImages().filter(this._isInView.bind(this))
}
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._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 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 newContributorTypeSelect = function(doc, changeListener) {
return elem('select')
.add([
elem('option').attr({value: 'all', selected: true}).add('全部').new(doc),
elem('option').attr('value', 'user').add('ユーザー').new(doc),
elem('option').attr('value', 'channel').add('チャンネル').new(doc),
])
.on('change', changeListener)
.new(doc)
}
var newVisitedMovieViewModeSelect = function(doc, changeListener) {
return elem('select')
.add([
elem('option').attr({value: 'reduce', selected: true}).add('縮小').new(doc),
elem('option').attr('value', 'hide').add('非表示').new(doc),
elem('option').attr('value', 'doNothing').add('通常表示').new(doc),
])
.on('change', changeListener)
.new(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 contributionDay = function (nicoChartDay) {
var r = /(\d{4})年(\d{2})月(\d{2})日 (\d{2}):(\d{2}):\d{2}/
.exec(nicoChartDay)
return `${r[1]}/${r[2]}/${r[3]} ${r[4]}:${r[5]}`
}
var hourlyRegExp = /^\/ranking\/(fav|view|res|mylist)\/hourly\/all/
var GinzaView = function(doc) {
doc = doc || document
this.doc = doc
this.visitedMovieViewModeSelect = newVisitedMovieViewModeSelect(
doc, this._setVisitedMovieViewMode.bind(this))
this.contributorTypeSelect = newContributorTypeSelect(
doc, this._setVisibleContributorType.bind(this))
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)
}
GinzaView.prototype.setModel = function(model) {
this.model = model
this.updateVisitedMovieViewMode()
this.updateNewWindowOpen()
this.updateMovieInfoTogglable()
this.updateSeamlessRankingNumber()
this.updateVisibleContributorType()
}
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(root) {
root = root || this.doc
return array(root.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(titleAnchor) {
return new MovieView(this, titleAnchor, this._getMovieRoot(titleAnchor))
}
GinzaView.prototype.movieIdInHRef = function(href) {
var execResult = /watch\/([^/?]+)/.exec(href)
return execResult ? execResult[1] : null
}
GinzaView.prototype.getMovies = function(root) {
var result = [], i2v = this.idToMovieView
this._getWatchAnchors(root).forEach(function(a) {
var id = this.movieIdInHRef(a.getAttribute('href'))
var movieView = i2v[id] = this._newMovieView(a)
result.push(new Movie(id, a.textContent, movieView))
}, this)
return result
}
GinzaView.prototype._newAddedController = function() {
return this.doc.createDocumentFragment()
}
GinzaView.prototype._newControllers = function() {
return elem('div')
.add([
elem('label').add('閲覧済みの動画を', this.visitedMovieViewModeSelect).new(this.doc),
' | ',
elem('label').add('投稿者', this.contributorTypeSelect).new(this.doc),
' | ',
elem('label').add(this.ngMovieVisibleCheckbox, ' NG動画を表示')
.new(this.doc),
' | ',
this._newAddedController(),
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.visitedMovieViewModeSelect.value =
this.model.visitedMovieViewMode().get().name()
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.updateVisibleContributorType = function() {
this.contributorTypeSelect.value =
this.model.visibleContributorType().get()
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() {
this.model.visitedMovieViewMode()
.set(ViewMode.get(this.visitedMovieViewModeSelect.value))
}
GinzaView.prototype._setVisibleContributorType = function() {
this.model.visibleContributorType()
.set(this.contributorTypeSelect.value)
}
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.updateSeamlessRankingNumber = function () {
var b = this.model.seamlessRankingNumber().get()
var e = this.doc.getElementById('nrn-seamlessRankingNumberStyle')
if (b && !e) {
var s = this.doc.createElement('style')
s.id = 'nrn-seamlessRankingNumberStyle'
s.textContent = this._getSeamlessRankingNumberCssText()
this.doc.head.appendChild(s)
} else if (!b && e) {
e.remove()
}
}
GinzaView.prototype._convertNicoChartTime = function(t) {
var r = /((\d+)時間)?((\d{1,2})分)?((\d{1,2})秒)?/.exec(t)
if (!(r && (r[1] || r[3] || r[5]))) return t
var result = ''
if (r[1]) result += r[2] + ':'
if (r[3]) {
result += (r[1] && r[4].length === 1 ? '0' : '') + r[4]
} else {
result += r[1] ? '00' : '0'
}
result += ':'
if (r[5]) {
result += (r[6].length === 1 ? '0' : '') + r[6]
} else {
result += '00'
}
return result
}
GinzaView.prototype.createMovieRoots = function (objs) {
var div = this.doc.createElement('div')
div.innerHTML = objs.map(function (o) {
var id = this.movieIdInHRef(o.url)
return `<li class="item videoRanking nrn-fromNicoChart" data-video-item data-enable-uad="1" data-id="${id}">
<div class="rankingNumWrap">
<p class="rankingNum">${o.rank}</p>
<p class="rankingPt">+${o.point}</p>
</div>
<div data-video-comments style="display: none;">
<p class="adComment" data-video-comments-inner></p>
</div>
<div class="videoList01Wrap">
<p class="itemTime${o.fresh ? ' new' : ''}"> <span>${contributionDay(o.contributionDay)}</span> 投稿</p>
<div class="itemThumbBox"><div class="itemThumb" data-video-thumbnail data-id="${id}"><a href="/watch/${id}" class="itemThumbWrap" data-link
data-href="/watch/${id}"><img src="http://res.nimg.jp/images/noimage.png" title="" alt="" class="noImage"><img class="jsLazyImage thumb" src="http://res.nimg.jp/images/1x1.gif" data-original="${o.thumbURL}" alt="${o.title}" data-thumbnail></a></div><span class="videoLength">${this._convertNicoChartTime(o.movieLength)}</span></div>
</div>
<div class="itemContent">
<p class="itemTitle ranking">
<a title="${o.title}" href="watch/${id}" data-href="watch/${id}">${o.title}</a>
</p>
<div class="wrap">
<p class="itemDescription ranking">${o.description}</p>
<p class="itemComment ranking">${o.comment}</p>
</div>
<div class="itemData">
<ul class="list">
<li class="count view">再生<span class="value">${o.viewCount}</span></li>
<li class="count comment">コメ<span class="value">${o.resCount}</span></li>
<li class="count mylist">マイ<span class="value"><a href="/mylistcomment/video/${id}">${o.mylistCount}</a></span></li>
<li class="count ads" style="display: none;" data-uad-point-outer>宣伝<span class="value"><a href="http://uad.nicovideo.jp/ads/?vid=so27132526&video_rank" data-uad-point>0</a></span></li>
</ul>
</div>
</div>
</li>`
}, this).join('')
return div
}
GinzaView.prototype._getSeamlessRankingNumberCssText = 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%;',
'}',
'.videoList01 .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;',
'}',
].join('\n') + `
.item.videoRanking.nrn-fromNicoChart .videoList01Wrap {
text-align: center;
}
.item.videoRanking.nrn-fromNicoChart .itemThumbBox {
display: inline-block;
}
.item.videoRanking.nrn-fromNicoChart .itemThumbBox,
.item.videoRanking.nrn-fromNicoChart .itemThumbBox .itemThumb,
.item.videoRanking.nrn-fromNicoChart .itemThumbBox .itemThumb .itemThumbWrap,
.item.videoRanking.nrn-fromNicoChart .itemThumbBox .itemThumb .itemThumbWrap .thumb {
height: 100px;
width: 130px;
}
.item.videoRanking.nrn-fromNicoChart .itemThumbBox .itemThumb .itemThumbWrap .thumb {
max-height: 100px;
}
.item.videoRanking.nrn-fromNicoChart.nrn-reduce .videoList01Wrap {
margin-left: 15px;
}
.item.videoRanking.nrn-fromNicoChart.nrn-reduce .itemThumbBox,
.item.videoRanking.nrn-fromNicoChart.nrn-reduce .itemThumbBox .itemThumb,
.item.videoRanking.nrn-fromNicoChart.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap,
.item.videoRanking.nrn-fromNicoChart.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap .thumb {
height: 50px;
width: 80px;
}
.item.videoRanking {
visibility: hidden;
}
.item.videoRanking.nrn-getThumbInfoDone {
visibility: inherit;
}
`
}
GinzaView.prototype.isHourlyAll = function () {
return hourlyRegExp.test(this.doc.location.pathname)
}
GinzaView.prototype.getSortType = function () {
var r = hourlyRegExp.exec(this.doc.location.pathname)
if (r && r[1]) return r[1] === 'fav' ? 'all' : r[1]
throw new Error(r && r[1])
}
GinzaView.prototype.requestToNicoChart = function (getThumbInfo, from) {
from = from || 101
var progress = elem('p').add(from + ' 位以降を取得中...').new(this.doc)
this.contentBody().appendChild(progress)
new NicoChart(GM_xmlhttpRequest, function (objs) {
progress.remove()
var div = this.createMovieRoots(objs.filter(function (o) {
return !this.idToMovieView[this.movieIdInHRef(o.url)]
}, this))
var movies = this.getMovies(div)
updateMovies(movies, this.model.stores())
this.model.addMovies(movies)
var target = this.doc.querySelector('.contentBody.video > .list')
array(div.childNodes).forEach(function (c) {
target.appendChild(c)
})
this.updateNewWindowOpen()
if (getThumbInfo) {
getThumbInfo.request(this.model.sortMoviesByVisible(movies))
} else {
movies.forEach(callMethod('setGetThumbInfoDone', []))
}
if (from === 101) {
this.requestToNicoChart(getThumbInfo, 201)
}
}.bind(this), function (message) {
progress.style.color = 'red'
progress.innerHTML =
`<a href="http://www.nicochart.jp/" target=_blank>ニコニコチャート</a>からの取得に失敗しました(${message})`
}).request({type: this.getSortType(), from: from})
}
GinzaView.prototype.updateMovingUp = noop
GinzaView.prototype.moveUp = noop
return GinzaView
})()
var MatrixView = (function (_super) {
var colToRows = function(idToMovieView) {
return Object.keys(idToMovieView).map(function(id) {
return idToMovieView[id]
}).reduce(function(o, v) {
var a = o[v.sourceCol]
if (a) a.push(v); else o[v.sourceCol] = [v]
return o
}, {})
}
var visibleComparator = function(a, b) {
var av = a.isVisible()
var bv = b.isVisible()
if (av && !bv) return -1
if (!av && bv) return 1
if (a.sourceRow < b.sourceRow) return -1
if (a.sourceRow > b.sourceRow) return 1
return 0
}
var sourceRowComparator = function(a, b) {
if (a.sourceRow < b.sourceRow) return -1
if (a.sourceRow > b.sourceRow) return 1
return 0
}
var rowToCols = function(colToRows) {
return Object.keys(colToRows).sort().map(function(col) {
return colToRows[col]
}).reduce(function(o, rows) {
return rows.reduce(function(o, r, i) {
var a = o[i]
if (a) a.push(r); else o[i] = [r]
return o
}, o)
}, [])
}
var currentRowComparator = function(a, b) {
if (a.currentRow() < b.currentRow()) return -1
if (a.currentRow() > b.currentRow()) return 1
return 0
}
var diffRowToCols = function(current, fresh) {
var result = []
for (var i = 0; i < current.length; i++) {
var cols = current[i]
var diff = []
for (var j = 0; j < cols.length; j++) {
var f = fresh[i][j]
if (cols[j].root === f.root) {
diff.push(null)
} else {
diff.push(f)
}
}
result.push(diff)
}
return result
}
var MatrixView = function () {
_super.apply(this, arguments)
this.movingUpCheckbox = elem('input')
.attr('type', 'checkbox')
.on('change', this._setMovingUp.bind(this))
.new(this.doc)
this._movingUpRequested = false
}
MatrixView.prototype._newMovingUpCheckbox = function() {
return elem('input').attr('type', 'checkbox').new(this.doc)
}
MatrixView.is = function (location) {
return location.pathname.startsWith('/ranking/matrix')
}
MatrixView.prototype = Object.create(_super.prototype)
MatrixView.prototype.setModel = function(model) {
_super.prototype.setModel.call(this, model)
this.movingUpCheckbox.checked = model.movingUp().get()
}
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;',
'}',
'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;',
'}',
'td[data-video-item].nrn-reduce .itemThumbBox,',
'td[data-video-item].nrn-reduce .itemThumbBox .itemThumb {',
' width: 50px;',
' height: 38px;',
' margin: 0;',
'}',
'.top20 .itemTitle a.nrn-ng-movie-title {',
' text-decoration: line-through;',
'}',
'td[data-video-item] {',
' position: relative;',
' visibility: hidden;',
'}',
'td[data-video-item].nrn-getThumbInfoDone {',
' visibility: inherit;',
'}',
'td[data-video-item].nrn-getThumbInfoDone.nrn-hidden {',
' visibility: hidden;',
'}',
'.nrn-hidden > .nrn-movie-info-container,',
'.nrn-hidden > .nrn-movie-info-toggle {',
' display: none;',
'}',
].join('\n')
}
MatrixView.prototype.updateMovieInfoTogglable = noop
MatrixView.prototype.isHourlyAll = always(false)
MatrixView.prototype._newAddedController = function() {
return elem()
.add([
elem('label').add(this.movingUpCheckbox, '上に詰める').new(this.doc),
' | ',
])
.new(this.doc)
}
MatrixView.prototype._setMovingUp = function() {
this.model.movingUp().set(this.movingUpCheckbox.checked)
}
MatrixView.prototype.updateMovingUp = function() {
if (this.model.movingUp().get()) {
this.moveUp()
} else {
this.unmoveUp()
}
}
MatrixView.prototype._sortedRowToCols = function(comparator) {
var c2r = colToRows(this.idToMovieView)
Object.keys(c2r).forEach(function(c) { c2r[c].sort(comparator) })
return rowToCols(c2r)
}
MatrixView.prototype._addToMatrixTable = function() {
var doc = this.doc
var rows = doc.querySelector('.top20 table').rows
return function(cols, i) {
var row = rows[1 + i]
cols.forEach(function(col, i) {
if (!col) return
if (col.root.parentNode) {
col.root.parentNode.replaceChild(doc.createElement('td'), col.root)
}
row.replaceChild(col.root, row.cells[i + 1])
})
}
}
MatrixView.prototype._relocate = function(comparator) {
var current = this._sortedRowToCols(currentRowComparator)
var fresh = this._sortedRowToCols(comparator)
diffRowToCols(current, fresh).forEach(this._addToMatrixTable())
}
MatrixView.prototype.moveUp = function() {
this._relocate(visibleComparator)
}
MatrixView.prototype.unmoveUp = function() {
this._relocate(sourceRowComparator)
}
MatrixView.prototype.requestMovingUp = function() {
if (this._movingUpRequested) return
this._movingUpRequested = true
setTimeout(function() {
this._movingUpRequested = false
if (this.model.movingUp().get()) this.moveUp()
}.bind(this))
}
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)
this.sourceRow = root.parentNode.rowIndex
this.sourceCol = root.cellIndex
}
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.isVisible = function() {
return !this.root.classList.contains('nrn-hidden')
}
MatrixMovieView.prototype.setVisible = function(visible) {
this.root.classList[visible ? 'remove' : 'add']('nrn-hidden')
this.view.requestMovingUp()
}
MatrixMovieView.prototype.getMovieDataElem = function() {
return this.root
}
MatrixMovieView.prototype.currentRow = function() {
return this.root.parentNode.rowIndex
}
MatrixMovieView.prototype._isMovieInfoTogglable = always(true)
MatrixMovieView.prototype.updateDescription = noop
return MatrixMovieView
})(MovieView)
var NicoChart = (function() {
var url = function (o) {
var t = o.type === 'all' ? '' : o.type
var p = o.from === 101 ? '' : 'page=2&'
return `http://now.nicochart.jp/hourly/${t}?${p}rank=-101`
}
var parseVideoInfo = function (e) {
var t = function (selector) {
var n = e.querySelector(selector)
return n ? n.textContent : ''
}
return {
point: e.parentNode.querySelector('.video-chart .point').textContent,
contributionDay: e.querySelector('.first-retrieve').firstChild.nodeValue,
movieLength: t('.length'),
viewCount: t('.view em'),
resCount: t('.res em'),
mylistCount: t('.mylist em'),
url: e.parentNode.querySelector('.thumbnail-image a').href,
thumbURL: e.parentNode.querySelector('.thumbnail-image a img').title,
title: t('.title'),
description: t('.description-summary'),
comment: t('.last-res-body'),
fresh: Boolean(e.querySelector('.first-retrieve.new')),
}
}
var parse = function (text, from) {
var d = new DOMParser().parseFromString(text, 'text/html')
var infos = d.querySelectorAll('#result .video-info')
if (infos.length === 0) {
throw new Error('"#result .video-info" query return empty')
}
return array(infos)
.map(parseVideoInfo)
.map(function (o) {
o.rank = from++
return o
})
}
var NicoChart = function(request, ok, fail) {
this._request = request
this._ok = ok
this._fail = fail
}
NicoChart.prototype._onload = function (from, r) {
if (r.status === 200) {
try {
this._ok(parse(r.responseText, from))
} catch (e) {
console.log(e)
this._fail(e.message)
}
} else {
this._fail(r.status + ' ' + r.statusText)
}
}
NicoChart.prototype._onerror = function (message) {
this._fail(message)
}
NicoChart.prototype.request = function (o) {
if (['all', 'view', 'res', 'mylist'].indexOf(o.type) === -1) {
throw new Error('o.type: ' + o.type)
}
if ([101, 201].indexOf(o.from) === -1) {
throw new Error('o.from: ' + o.from)
}
this._request({
method: 'GET',
timeout: 30000,
url: url(o),
onload: this._onload.bind(this, o.from),
onerror: this._onerror.bind(this, 'エラー'),
ontimeout: this._onerror.bind(this, 'タイムアウト'),
})
}
return NicoChart
})()
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 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 initCheckbox = function(checkbox, checked, changeListener) {
checkbox.checked = checked
checkbox.addEventListener('change', changeListener)
}
var byId = function(id) {
return function() { return this.doc.getElementById(id) }
}
var ConfigDialog = function(model, doc) {
this.model = model
this.doc = doc
this.background = newBackground(ConfigDialog.Z_INDEX - 1)
this.iframe = null
this.initListSelect()
this.getTargetSelect()
.addEventListener('change', this.updateList.bind(this))
this.getAddButton()
.addEventListener('click', this.addPromptResult.bind(this))
this.getRemoveButton()
.addEventListener('click', this.removeSelectedItems.bind(this))
this.getRemoveAllButton()
.addEventListener('click', this.removeAll.bind(this))
this.getOpenButton()
.addEventListener('click', this.openSelectedItems.bind(this))
this.getCloseButton()
.addEventListener('click', this.remove.bind(this))
this.updateButtonDisabled()
initCheckbox(this.getNewWindowOpenCheckbox()
, this.model.openNewWindow().get()
, this.updateNewWindowOpen.bind(this))
initCheckbox(this.getUseGetThumbInfoCheckbox()
, this.model.useGetThumbInfo().get()
, this.updateUseGetThumbInfo.bind(this))
initCheckbox(this.getSeamlessRankingNumberCheckbox()
, this.model.seamlessRankingNumber().get()
, this.updateSeamlessRankingNumber.bind(this))
initCheckbox(this.getRequestingNextCheckbox()
, this.model.requestingNext().get()
, this.updateRequestingNext.bind(this))
initCheckbox(this.getMovieInfoTogglableCheckbox()
, this.model.movieInfoTogglable().get()
, this.updateMovieInfoTogglable.bind(this))
initCheckbox(this.getDescriptionTogglableCheckbox()
, this.model.descriptionTogglable().get()
, this.updateDescriptionTogglable.bind(this))
initCheckbox(this.getPopupVisibleCheckbox()
, this.model.popupVisible().get()
, this.updatePopupVisible.bind(this))
}
ConfigDialog.Z_INDEX = 10000
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;
}
</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-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=newWindowOpen>動画を別窓で開く</label></p>
<p><label><input type=checkbox id=useGetThumbInfo>動画情報を取得する</label></p>
<p><label><input type=checkbox id=seamlessRankingNumber>表示されている動画で順位を数える</label></p>
<p><label><input type=checkbox id=requestingNext>カテゴリ合算毎時ランキングの 101 位以降を取得する</label></p>
<p class=sideComment><small>取得元: <a target=_blank href=http://www.nicochart.jp/>ニコニコチャート</a></small></p>
<p><label><input type=checkbox id=popupVisible>ポップアップを表示する</label></p>
<fieldset>
<legend>表示・非表示の切り替えボタン</legend>
<p><label><input type=checkbox id=movieInfoTogglable>タグ、ユーザー、チャンネル</label></p>
<p><label><input type=checkbox id=descriptionTogglable>動画説明</label></p>
</fieldset>
<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>`
ConfigDialog.show = function(model, callback) {
var f = document.createElement('iframe')
f.srcdoc = ConfigDialog.srcdoc
f.addEventListener('load', function() {
var d = new ConfigDialog(model, f.contentDocument)
d.setup(f)
if (callback) callback(d)
})
document.body.appendChild(f)
}
ConfigDialog.prototype.getNewWindowOpenCheckbox = byId('newWindowOpen')
ConfigDialog.prototype.getUseGetThumbInfoCheckbox = byId('useGetThumbInfo')
ConfigDialog.prototype.getSeamlessRankingNumberCheckbox = byId('seamlessRankingNumber')
ConfigDialog.prototype.getRequestingNextCheckbox = byId('requestingNext')
ConfigDialog.prototype.getMovieInfoTogglableCheckbox = byId('movieInfoTogglable')
ConfigDialog.prototype.getDescriptionTogglableCheckbox = byId('descriptionTogglable')
ConfigDialog.prototype.getPopupVisibleCheckbox = byId('popupVisible')
ConfigDialog.prototype.getAddButton = byId('addButton')
ConfigDialog.prototype.getRemoveButton = byId('removeButton')
ConfigDialog.prototype.getRemoveAllButton = byId('removeAllButton')
ConfigDialog.prototype.getOpenButton = byId('openButton')
ConfigDialog.prototype.getCloseButton = byId('closeButton')
ConfigDialog.prototype.getTargetSelect = byId('target')
ConfigDialog.prototype.getListSelect = byId('list')
ConfigDialog.prototype.initListSelect = function() {
var l = this.getListSelect()
elem.add(l, this.model.ngTitles().get().map(function(t) {
return opt(t, t).new(this.doc)
}, this))
l.addEventListener('change', this.updateButtonDisabled.bind(this))
}
ConfigDialog.prototype.setup = function(iframe) {
document.body.appendChild(this.background)
this.iframe = elem.css(iframe, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
'z-index': ConfigDialog.Z_INDEX,
})
}
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.getTargetSelect().options[this.getTargetSelect().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.getListSelect().appendChild(opt(r, r).new(this.doc))
this.updateButtonDisabled()
}
ConfigDialog.prototype.removeSelectedItems = function() {
var opts = [].slice.call(this.getListSelect().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.getListSelect().options).forEach(function(o) {
o.parentNode.removeChild(o)
})
this.updateButtonDisabled()
}
ConfigDialog.prototype.openSelectedItems = function() {
var a = action(this.selectedTargetOption())
;[].forEach.call(this.getListSelect().selectedOptions, function(o) {
GM_openInTab(a.url(o.value))
})
}
ConfigDialog.prototype.updateList = function() {
;[].slice.call(this.getListSelect().options).forEach(function(o) {
o.parentNode.removeChild(o)
})
var o = this.selectedTargetOption()
action(o).get(this.model).forEach(function(v) {
this.getListSelect().appendChild(opt(v, v).new(this.doc))
}, this)
this.updateButtonDisabled()
}
ConfigDialog.prototype.updateButtonDisabled = function() {
this.getRemoveAllButton().disabled = !this.getListSelect().options.length
var disabled = this.getListSelect().selectedIndex === -1
this.getRemoveButton().disabled = disabled
this.getOpenButton().disabled = disabled
}
ConfigDialog.prototype.updateNewWindowOpen = function() {
this.model.openNewWindow().set(this.getNewWindowOpenCheckbox().checked)
}
ConfigDialog.prototype.updateSeamlessRankingNumber = function () {
this.model.seamlessRankingNumber()
.set(this.getSeamlessRankingNumberCheckbox().checked)
}
ConfigDialog.prototype.updateUseGetThumbInfo = function() {
this.model.useGetThumbInfo()
.set(this.getUseGetThumbInfoCheckbox().checked)
}
ConfigDialog.prototype.updateRequestingNext = function() {
this.model.requestingNext().set(this.getRequestingNextCheckbox().checked)
}
ConfigDialog.prototype.updateMovieInfoTogglable = function() {
this.model.movieInfoTogglable()
.set(this.getMovieInfoTogglableCheckbox().checked)
}
ConfigDialog.prototype.updateDescriptionTogglable = function() {
this.model.descriptionTogglable()
.set(this.getDescriptionTogglableCheckbox().checked)
}
ConfigDialog.prototype.updatePopupVisible = function() {
this.model.popupVisible().set(this.getPopupVisibleCheckbox().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) {
if (!this._model.popupVisible().get()) return
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())
document.addEventListener('DOMContentLoaded', function() {
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))
var getThumbInfo = null
if (stores.useGetThumbInfo.get()) {
getThumbInfo = new GetThumbInfo(GM_xmlhttpRequest
, model.sortMoviesByVisible()
, stores
, 5)
getThumbInfo.request()
} else {
movies.forEach(callMethod('setGetThumbInfoDone', []))
}
if (stores.requestingNext.get() && view.isHourlyAll()) {
view.requestToNicoChart(getThumbInfo)
}
})
}
main()
})()