// ==UserScript==
// @name Nico Nico Ranking NG
// @namespace http://userscripts.org/users/121129
// @description ニコニコ動画のランキングにNG機能を追加
// @match http://www.nicovideo.jp/ranking*
// @version 9
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @license MIT License
// @noframes
// ==/UserScript==
;(function() {
'use strict'
function removeAllChild(parent) {
while (parent.firstChild) parent.removeChild(parent.firstChild)
}
var elem = (function() {
function setter(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
}
}
function Builder(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: !!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] = !!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(function(c) {
f.appendChild(c)
})
elem.appendChild(f)
return elem
}
return elem
})()
function array(likeArray) {
return Array.prototype.slice.call(likeArray)
}
var Movie = (function() {
function Movie(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.user = { id: -1, name: '', ng: false }
this.channel = { id: -1, name: '', ng: false }
this.error = ''
this.description = ''
}
Movie.prototype.setVisited = function(visited) {
if (this.visited !== visited) {
this.visited = visited
this.view.updateVisited(this)
}
}
Movie.prototype.setVisitedByRegExp = function(regExp) {
this.setVisited(regExp.test(this.id))
}
Movie.prototype.setNgId = function(ngId) {
if (this.ngId !== ngId) {
this.ngId = ngId
this.view.updateNgId(this)
}
}
Movie.prototype.setNgIdByRegExp = function(regExp) {
this.setNgId(regExp.test(this.id))
}
Movie.prototype.setNgTitleByRegExp = function(regExp) {
var execResult = regExp.exec(this.title)
var preNgTitle = this.ngTitle
this.ngTitle = Boolean(execResult)
var preMatchedNgTitle = this.matchedNgTitle
this.matchedNgTitle = execResult ? execResult[0] : ''
if (preNgTitle !== this.ngTitle)
this.view.updateNgTitle(this)
else if (preNgTitle
&& this.ngTitle
&& preMatchedNgTitle !== this.matchedNgTitle)
this.view.updateMatchedNgTitle(this)
}
Movie.prototype.isNG = function() {
return this.ngId
|| this.ngTitle
|| Boolean(this.ngTags.length)
|| this.user.ng
|| this.channel.ng
}
function equalArray(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
}
Movie.prototype.setTags = function(tags) {
if (!equalArray(this.tags, tags)) {
this.tags = tags
this.view.updateTags(this)
}
}
Movie.prototype.setNgTagsByRegExp = function(regExp) {
var ngTags = this.tags.filter(function(tag) { return regExp.test(tag) })
if (!equalArray(this.ngTags, ngTags)) {
this.ngTags = ngTags
this.view.updateNgTags(this)
}
}
Movie.prototype.setUser = function(id, name) {
if (this.user.id !== id || this.user.name !== name) {
this.user.id = id
this.user.name = name
this.view.updateUser(this)
}
}
Movie.prototype.setNgUserByIds = function(ids) {
var ng = (ids.indexOf(this.user.id) !== -1)
if (this.user.ng !== ng) {
this.user.ng = ng
this.view.updateNgUser(this)
}
}
Movie.prototype.setChannel = function(id, name) {
if (this.channel.id !== id || this.channel.name !== name) {
this.channel.id = id
this.channel.name = name
this.view.updateChannel(this)
}
}
Movie.prototype.setNgChannelByIds = function(ids) {
var ng = (ids.indexOf(this.channel.id) !== -1)
if (this.channel.ng !== ng) {
this.channel.ng = ng
this.view.updateNgChannel(this)
}
}
Movie.prototype.setError = function(error) {
if (this.error !== error) {
this.error = error
this.view.updateError(this)
}
}
Movie.prototype.hasExternalInfo = function() {
return Boolean(this.tags.length)
|| this.user.id !== -1
|| this.channel.id !== -1
}
Movie.prototype.setDescription = function(description) {
if (this.description !== description) {
this.description = description
this.view.updateDescription(this)
}
}
return Movie
})()
var Model = (function() {
function newIdToMovieMap(movies) {
var result = Object.create(null)
movies.forEach(function(movie) { result[movie.id] = movie })
return result
}
function GM_getArray(name) {
return JSON.parse(GM_getValue(name, '[]'))
}
function containAsCaseInsensitive(array, value) {
var re = new RegExp('^' + escapeNoWordChars(value) + '$', 'i')
return array.some(function(e) { return re.test(e) })
}
function GM_addToArray(name, value) {
var values = GM_getArray(name)
if (containAsCaseInsensitive(values, value)) return false
values.push(value)
GM_setValue(name, JSON.stringify(values))
return true
}
function GM_removeFromArray(name, value) {
value = Array.isArray(value) ? value : [value]
var newValue = GM_getArray(name).filter(function(element) {
return value.indexOf(element) === -1
})
GM_setValue(name, JSON.stringify(newValue))
}
function escapeNoWordChars(s) {
return String(s).replace(/\W/g, '\\$&')
}
function newPattern(elements) {
var s = '('
elements.forEach(function(e) { s += escapeNoWordChars(e) + '|' })
return s.slice(0, -1) + ')'
}
function newStrictPattern(elements) {
return '^' + newPattern(elements) + '$'
}
function selectorInThumb(selector) {
return 'nicovideo_thumb_response > thumb > ' + selector
}
function queryTags(doc) {
var s = selectorInThumb('tags > tag')
return array(doc.querySelectorAll(s)).map(function(tag) {
return tag.textContent
})
}
function queryInt(doc, selector) {
var n = doc.querySelector(selectorInThumb(selector))
if (!n) return -1
var i = parseInt(n.textContent)
return isNaN(i) ? -1 : i
}
function queryUserId(doc) {
return queryInt(doc, 'user_id')
}
function queryUserName(doc) {
var n = doc.querySelector(selectorInThumb('user_nickname'))
return n ? n.textContent : ''
}
function queryChannelId(doc) {
return queryInt(doc, 'ch_id')
}
function queryChannelName(doc) {
var n = doc.querySelector(selectorInThumb('ch_name'))
return n ? n.textContent : ''
}
function queryDescription(doc) {
var n = doc.querySelector(selectorInThumb('description'))
return n ? n.textContent : ''
}
function setError(movie, error, next) {
movie.setError(error)
next()
}
function Model(movies, view) {
this.movies = movies
this.view = view
this.ngMovieVisible = false
this.idToMovie = newIdToMovieMap(movies)
}
Model.NEVER_MATCHED_REGEXP = /^[^\d\D]/
Model.prototype.getVisitedMovieViewMode = function() {
return ViewMode.get(GM_getValue('visitedMovieViewMode', 'reduce'))
}
Model.prototype.setVisitedMovieViewMode = function(viewMode) {
if (!this.getVisitedMovieViewMode().hasSameName(viewMode)) {
GM_setValue('visitedMovieViewMode', viewMode.name)
this.view.updateVisitedMovieViewMode()
}
}
Model.prototype.setNgMovieVisible = function(ngMovieVisible) {
if (this.ngMovieVisible !== ngMovieVisible) {
this.ngMovieVisible = ngMovieVisible
this.view.updateNgMovieVisible()
}
}
Model.prototype.isNewWindowOpen = function() {
return GM_getValue('openNewWindow', true)
}
Model.prototype.setNewWindowOpen = function(newWindowOpen) {
if (this.isNewWindowOpen() !== newWindowOpen) {
GM_setValue('openNewWindow', newWindowOpen)
this.view.updateNewWindowOpen()
}
}
Model.prototype.isUseGetThumbInfo = function() {
return GM_getValue('useGetThumbInfo', true)
}
Model.prototype.setUseGetThumbInfo = function(useGetThumbInfo) {
if (this.isUseGetThumbInfo() !== useGetThumbInfo)
GM_setValue('useGetThumbInfo', useGetThumbInfo)
}
Model.prototype.isMovieInfoTogglable = function() {
return GM_getValue('movieInfoTogglable', true)
}
Model.prototype.setMovieInfoTogglable = function(movieInfoTogglable) {
if (this.isMovieInfoTogglable() !== movieInfoTogglable) {
GM_setValue('movieInfoTogglable', movieInfoTogglable)
this.view.updateMovieInfoTogglable()
}
}
Model.prototype.isDescriptionTogglable = function() {
return GM_getValue('descriptionTogglable', true)
}
Model.prototype.setDescriptionTogglable = function(descriptionTogglable) {
if (this.isDescriptionTogglable() !== descriptionTogglable) {
GM_setValue('descriptionTogglable', descriptionTogglable)
this.view.updateDescriptionTogglable()
}
}
Model.prototype.getVisitedIds = function() {
return GM_getArray('visitedMovies')
}
Model.prototype.addVisitedId = function(movieId) {
if (GM_addToArray('visitedMovies', movieId)) {
if (this.idToMovie[movieId]) this.idToMovie[movieId].setVisited(true)
return true
}
return false
}
Model.prototype.removeVisitedId = function(/* id... */) {
var ids = [].concat.apply([], arguments)
GM_removeFromArray('visitedMovies', ids)
ids.forEach(function(id) {
if (this.idToMovie[id]) this.idToMovie[id].setVisited(false)
}, this)
}
Model.prototype.clearVisitedIds = function() {
GM_deleteValue('visitedMovies')
this.movies.forEach(function(movie) { movie.setVisited(false) })
}
Model.prototype.getNgIds = function() { return GM_getArray('ngMovies') }
Model.prototype.addNgId = function(movieId) {
if (GM_addToArray('ngMovies', movieId)) {
if (this.idToMovie[movieId]) this.idToMovie[movieId].setNgId(true)
return true
}
return false
}
Model.prototype.removeNgId = function(/* id... */) {
var ids = [].concat.apply([], arguments)
GM_removeFromArray('ngMovies', ids)
ids.forEach(function(id) {
if (this.idToMovie[id]) this.idToMovie[id].setNgId(false)
}, this)
}
Model.prototype.clearNgIds = function() {
GM_deleteValue('ngMovies')
this.movies.forEach(function(movie) { movie.setNgId(false) })
}
Model.prototype.getNgTitles = function() {
return GM_getArray('ngTitles')
}
Model.prototype.addNgTitle = function(movieTitle) {
if (GM_addToArray('ngTitles', movieTitle)) {
var regExp = new RegExp(escapeNoWordChars(movieTitle), 'i')
this.movies.forEach(function(movie) {
if (!movie.ngTitle) movie.setNgTitleByRegExp(regExp)
})
return true
}
return false
}
Model.prototype.removeNgTitle = function(movieTitle) {
this.removeNgTitles([movieTitle])
}
Model.prototype.removeNgTitles = function(movieTitles, ignoreCase) {
ignoreCase = ignoreCase === undefined ? true : ignoreCase
var re = new RegExp(newStrictPattern(movieTitles), ignoreCase ? 'i' : '')
var newValue = this.getNgTitles().filter(function(element) {
return !re.test(element)
})
GM_setValue('ngTitles', JSON.stringify(newValue))
this.updateAllNgTitles()
}
Model.prototype.clearNgTitles = function() {
GM_deleteValue('ngTitles')
this.movies.forEach(function(movie) {
movie.setNgTitleByRegExp(Model.NEVER_MATCHED_REGEXP)
})
}
Model.prototype.getNgTags = function() { return GM_getArray('ngTags') }
Model.prototype.addNgTag = function(movieTag) {
if (GM_addToArray('ngTags', movieTag)) {
this.updateAllNgTags()
return true
}
return false
}
Model.prototype.removeNgTag = function(movieTag) {
this.removeNgTags([movieTag])
}
Model.prototype.removeNgTags = function(movieTags, ignoreCase) {
ignoreCase = ignoreCase === undefined ? true : ignoreCase
var re = new RegExp(newStrictPattern(movieTags), ignoreCase ? 'i' : '')
var newValue = this.getNgTags().filter(function(element) {
return !re.test(element)
})
GM_setValue('ngTags', JSON.stringify(newValue))
this.updateAllNgTags()
}
Model.prototype.clearNgTags = function() {
GM_deleteValue('ngTags')
this.movies.forEach(function(movie) {
movie.setNgTagsByRegExp(Model.NEVER_MATCHED_REGEXP)
})
}
Model.prototype.getNgUserIds = function() {
return GM_getArray('ngUserIds')
}
Model.prototype.addNgUserId = function(ngUserId) {
if (GM_addToArray('ngUserIds', ngUserId)) {
this.updateAllNgUser()
return true
}
return false
}
Model.prototype.removeNgUserId = function(/* id... */) {
GM_removeFromArray('ngUserIds', [].concat.apply([], arguments))
this.updateAllNgUser()
}
Model.prototype.clearNgUserIds = function() {
GM_deleteValue('ngUserIds')
this.updateAllNgUser()
}
Model.prototype.getNgChannelIds = function() {
return GM_getArray('ngChannelIds')
}
Model.prototype.addNgChannelId = function(id) {
if (GM_addToArray('ngChannelIds', id)) {
this.updateAllNgChannel()
return true
}
return false
}
Model.prototype.removeNgChannelId = function(/* id... */) {
GM_removeFromArray('ngChannelIds', [].concat.apply([], arguments))
this.updateAllNgChannel()
}
Model.prototype.clearNgChannelIds = function() {
GM_deleteValue('ngChannelIds')
this.updateAllNgChannel()
}
Model.prototype.updateAllVisited = function() {
var visitedIds = this.getVisitedIds()
if (visitedIds.length) {
var re = new RegExp(newStrictPattern(visitedIds))
this.movies.forEach(function(movie) { movie.setVisitedByRegExp(re) })
} else {
this.movies.forEach(function(movie) { movie.setVisited(false) })
}
}
Model.prototype.updateAllNgIds = function() {
var ngIds = this.getNgIds()
if (ngIds.length) {
var re = new RegExp(newStrictPattern(ngIds))
this.movies.forEach(function(movie) { movie.setNgIdByRegExp(re) })
} else {
this.movies.forEach(function(movie) { movie.setNgId(false) })
}
}
Model.prototype.updateAllNgTitles = function() {
var ngTitles = this.getNgTitles()
var re = ngTitles.length ? new RegExp(newPattern(ngTitles), 'i')
: Model.NEVER_MATCHED_REGEXP
this.movies.forEach(function(movie) { movie.setNgTitleByRegExp(re) })
}
Model.prototype.updateAllNgTags = function() {
var ngTags = this.getNgTags()
var re = ngTags.length ? new RegExp(newStrictPattern(ngTags), 'i')
: Model.NEVER_MATCHED_REGEXP
this.movies.forEach(function(movie) { movie.setNgTagsByRegExp(re) })
}
Model.prototype.updateAllNgUser = function() {
var ngUserIds = this.getNgUserIds()
this.movies.forEach(function(movie) {
movie.setNgUserByIds(ngUserIds)
})
}
Model.prototype.updateAllNgChannel = function() {
var ids = this.getNgChannelIds()
this.movies.forEach(function(movie) {
movie.setNgChannelByIds(ids)
})
}
Model.prototype.isVisible = function(movie) {
return !movie.isNG() || this.ngMovieVisible
}
Model.prototype.isHidden = function(movie) {
return !this.isVisible(movie)
|| (movie.visited && this.getVisitedMovieViewMode().isHideMode())
}
Model.prototype.isReduced = function(movie) {
return this.isVisible(movie)
&& movie.visited
&& this.getVisitedMovieViewMode().isReduceMode()
}
Model.prototype.getMovieViewMode = function(movie) {
if (this.isHidden(movie)) return new ViewMode.Hide()
if (this.isReduced(movie)) return new ViewMode.Reduce()
return new ViewMode.DoNothing()
}
Model.prototype.request = function(movie, next) {
GM_xmlhttpRequest({
method: 'GET',
url: 'http://ext.nicovideo.jp/api/getthumbinfo/' + movie.id,
timeout: 5000,
onload: this.evalResOrSetError.bind(this, movie, next),
onerror: setError.bind(null, movie, 'エラー', next),
ontimeout: setError.bind(null, movie, 'タイムアウト', next),
})
}
Model.prototype.evalResOrSetError = function(movie, next, res) {
var HTTP_OK = 200
if (res.status === HTTP_OK) this.evalRes(movie, res)
else movie.setError(res.statusText)
next()
}
Model.prototype.evalRes = function(movie, res) {
var d = new DOMParser().parseFromString(res.responseText
, 'application/xml')
movie.setTags(queryTags(d))
movie.setUser(queryUserId(d), queryUserName(d))
movie.setChannel(queryChannelId(d), queryChannelName(d))
movie.setDescription(queryDescription(d))
var ngTags = this.getNgTags()
if (ngTags.length)
movie.setNgTagsByRegExp(new RegExp(newStrictPattern(ngTags), 'i'))
movie.setNgUserByIds(this.getNgUserIds())
movie.setNgChannelByIds(this.getNgChannelIds())
}
Model.prototype.chainRequest = function(movies) {
return movies.reduceRight((function(next, movie) {
return this.request.bind(this, movie, next)
}).bind(this), function() {})
}
Model.prototype.postponeHiddenMovies = 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(function(e) { return e.movie })
}
Model.prototype.requestGetThumbInfo = function() {
if (this.isUseGetThumbInfo()) {
this.chainRequest(this.postponeHiddenMovies())()
}
}
return Model
})()
var Action = {}
Action.ADD =
{ ngIdButtonText: 'NG登録'
, ngTitleButtonText: 'NGタイトル追加'
, visitButtonText: '閲覧済み'
, doVisitedId: function(model, movieId) { model.addVisitedId(movieId) }
, doNgId: function(model, movieId) { model.addNgId(movieId) }
, doNgTitle: function(model, movieId) {
var r = null
do {
var msg = (r ? '"' + r + '"は登録済みです。\n' : '')
+ 'NGタイトルを入力'
r = prompt(msg, r ? r : model.idToMovie[movieId].title)
} while (r && !model.addNgTitle(r))
}
, doNgTag: function(model, tag) { model.addNgTag(tag) }
, doNgUserId: function(model, movieId) {
model.addNgUserId(model.idToMovie[movieId].user.id)
}
, doNgChannelId: function(model, movieId) {
model.addNgChannelId(model.idToMovie[movieId].channel.id)
}
}
Action.REMOVE =
{ ngIdButtonText: 'NG解除'
, ngTitleButtonText: 'NGタイトル削除'
, visitButtonText: '未閲覧'
, doVisitedId: function(model, movieId) { model.removeVisitedId(movieId) }
, doNgId: function(model, movieId) { model.removeNgId(movieId) }
, doNgTitle: function(model, movieId) {
model.removeNgTitle(model.idToMovie[movieId].matchedNgTitle)
}
, doNgTag: function(model, tag) { model.removeNgTag(tag) }
, doNgUserId: function(model, movieId) {
model.removeNgUserId(model.idToMovie[movieId].user.id)
}
, doNgChannelId: function(model, movieId) {
model.removeNgChannelId(model.idToMovie[movieId].channel.id)
}
}
function ViewMode() {}
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 = new ViewMode()
ViewMode.DoNothing.prototype.name = 'doNothing'
ViewMode.DoNothing.prototype.restoreViewMode = function(movieView) {}
ViewMode.DoNothing.prototype.setViewMode = function(movieView) {}
ViewMode.DoNothing.prototype.updateView = function(view) {
view.doNothingRadio.checked = true
}
ViewMode.Hide = function() {}
ViewMode.Hide.prototype = new ViewMode()
ViewMode.Hide.prototype.name = '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() {
function setStyle(root, obj) {
var r = root, o = obj
;['.rankingPt', '.itemTime', '.wrap', '.itemData'].forEach(function(s) {
r.querySelector(s).style.display = o.display
})
r.querySelector('.rankingNum').style.fontSize = o.rankingNumFontSize
r.querySelector('.videoList01Wrap').style.width = o.videoListWrapWidth
r.querySelector('.itemTitle').style.width = o.itemTitleWidth
setItemThumbSize(r, '.itemThumb', o.itemThumb)
setItemThumbSize(r, '.itemThumbWrap', o.itemThumb)
setItemThumbSize(r, '.itemThumbBox', o.itemThumb)
}
function setItemThumbSize(root, selector, size) {
var itemThumb = root.querySelector(selector)
itemThumb.style.width = size.width
itemThumb.style.height = size.height
}
function Reduce() {}
Reduce.prototype = new ViewMode()
Reduce.prototype.name = 'reduce'
Reduce.prototype.restoreViewMode = function(movieView) {
var o = { display: ''
, rankingNumFontSize: ''
, videoListWrapWidth: ''
, itemTitleWidth: ''
, itemThumb: { width: '', height: '' }
}
var r = movieView.root
setStyle(r, o)
this.restoreThumb(r.querySelector('.thumb'))
movieView.movieInfo.container.style.display = ''
}
Reduce.prototype.setViewMode = function(movieView) {
var o = { display: 'none'
, rankingNumFontSize: '150%'
, videoListWrapWidth: '80px'
, itemTitleWidth: 'auto'
, itemThumb: { width: '80px', height: '45px' }
}
var r = movieView.root
setStyle(r, o)
this.halfThumb(r.querySelector('.thumb'))
movieView.movieInfo.container.style.display = 'none'
}
Reduce.prototype.updateView = function(view) {
view.reduceRadio.checked = true
}
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%'
}
}
Reduce.prototype.restoreThumb = function(thumb) {
if (this.srcThumbWidth) {
thumb.style.width = this.srcThumbWidth + 'px'
thumb.style.marginTop = this.srcThumbMarginTop + 'px'
} else {
thumb.style.width = ''
}
}
return Reduce
})()
var MovieView = (function() {
var movieInfoAnchorColor = '#333333'
function newParagraph() {
return elem('p')
.attr('class', 'font12')
.css({'margin-top': '4px', 'line-height': '1.5em'})
.add([].slice.call(arguments))
.new()
}
function newActionButton(clickCallback) {
return elem('span')
.css('cursor', 'pointer')
.add('[+]')
.on('click', clickCallback)
.new()
}
function anchor(text, clickCallback) {
return elem('a')
.attr({'href': 'javascript:void(0)', 'style': 'color:#FFF;'})
.add(text)
.on('click', clickCallback)
.new()
}
function newNgIdButton(clickCallback) {
return anchor(Action.ADD.ngIdButtonText, clickCallback)
}
function newNgTitleButton(clickCallback) {
return anchor(Action.ADD.ngTitleButtonText, clickCallback)
}
function newVisitButton(clickCallback) {
return anchor(Action.ADD.visitButtonText, clickCallback)
}
var OPEN_TOGGLE_TEXT = '開く▼'
var CLOSE_TOGGLE_TEXT = '閉じる▲'
var TOGGLE_STYLE = {'cursor': 'pointer', 'text-decoration': 'underline'}
var ABSOLUTE_TOP_RIGHT = {position: 'absolute', top: '0px', right: '0px'}
function newMovieInfoToggleButton(clickCallback) {
return elem('span')
.attr('class', 'count')
.css(ABSOLUTE_TOP_RIGHT)
.on('click', clickCallback)
.add(elem('span')
.attr('class', 'value')
.css(TOGGLE_STYLE)
.css('padding', '0px')
.add(OPEN_TOGGLE_TEXT)
.new())
.new()
}
function newDescriptionToggleButton(clickCallback) {
return elem('span')
.css(TOGGLE_STYLE)
.on('click', clickCallback)
.add(OPEN_TOGGLE_TEXT)
.new()
}
function newDescriptionToggleP(toggle) {
return elem('p')
.css(ABSOLUTE_TOP_RIGHT)
.css('text-align', 'right')
.add(toggle)
.new()
}
function newErrorElem() {
return elem('li').attr('class', 'count').css('color', 'red').new()
}
var decodeHtmlCharRef = (function() {
var e = document.createElement('span')
return function(text) {
e.innerHTML = text
return e.textContent
}
})()
function newTagAnchor(tag) {
return elem('a')
.attr({
href: 'http://www.nicovideo.jp/tag/' + tag,
target: '_blank',
})
.css('color', movieInfoAnchorColor)
.add(decodeHtmlCharRef(tag))
.new()
}
function newTagSpan(anchor, button) {
return elem('span')
.css({'white-space': 'nowrap', 'margin-right': '0.5em'})
.add(anchor, button)
.new()
}
function movieInfoAnchorStyle(ng) {
return {
color: ng ? 'white' : movieInfoAnchorColor,
'background-color': ng ? 'fuchsia' : '',
}
}
function setTagActionAndStyle(tagView, ng) {
tagView.action = ng ? Action.REMOVE : Action.ADD
elem.css(tagView.anchor, movieInfoAnchorStyle(ng))
tagView.button.textContent = ng ? '[x]' : '[+]'
}
function newActionMenu(visitButton, ngIdButton, ngTitleButton) {
return elem('div')
.css({
position: 'absolute',
top: '10px',
right: '0px',
padding: '3px',
color: '#999',
'background-color': 'rgb(105, 105, 105)',
display: 'none',
})
.add(visitButton, ' | ', ngIdButton, ' | ', ngTitleButton)
.new()
}
function setContributorStyle(anchor, button, ng) {
elem.css(anchor, movieInfoAnchorStyle(ng))
button.textContent = ng ? '[x]' : '[+]'
}
function newContributorA() {
return elem('a')
.attr('target', '_blank')
.css('color', movieInfoAnchorColor)
.new()
}
function Description(movieView, callbacks) {
this.movieView = movieView
this.expanded = false
this.hasBeenSet = false
this.p = elem('p').new()
this.toggle = newDescriptionToggleButton(callbacks.descriptionToggle)
this.toggleP = newDescriptionToggleP(this.toggle)
}
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)
elem.css(e, {height: 'auto', position: 'relative'})
e.appendChild(this.p)
this.hasBeenSet = true
}
Description.prototype.addToggle = function() {
this.container().appendChild(this.toggleP)
}
Description.prototype.removeToggle = function() {
var p = this.toggleP
if (p.parentNode) p.parentNode.removeChild(p)
}
Description.prototype.setExpanded = function(expanded) {
this.expanded = expanded
elem.css(this.p, {
height: expanded ? 'auto' : '14px',
width: expanded ? '440px' : '400px',
})
elem.css(this.toggleP, {
position: expanded ? '' : 'absolute',
top: expanded ? '' : '0px',
right: expanded ? '' : '0px',
})
this.toggle.textContent = expanded ? CLOSE_TOGGLE_TEXT : OPEN_TOGGLE_TEXT
}
Description.prototype.linkify = (function() {
var re2preHRef = [
[/https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, ''],
[/sm\d+/g, 'http://www.nicovideo.jp/watch/'],
[/so\d+/g, 'http://www.nicovideo.jp/watch/'],
[/co\d+/g, 'http://com.nicovideo.jp/community/'],
[/ar\d+/g, 'http://ch.nicovideo.jp/article/'],
[/im\d+/g, 'http://seiga.nicovideo.jp/seiga/'],
[/lv\d+/g, 'http://live.nicovideo.jp/watch/'],
[/mylist\/\d+/g, 'http://www.nicovideo.jp/'],
[/watch\/\d+/g, 'http://www.nicovideo.jp/'],
[/user\/\d+/g, 'http://www.nicovideo.jp/'],
]
function linkify(text, re, preHRef) {
var result = elem().new()
var lastIndex = 0
for (var r; r = re.exec(text);) {
elem.add(result, [
text.slice(lastIndex, r.index),
elem('a')
.attr({target: '_blank', href: preHRef + r[0]})
.add(r[0])
.new(),
])
lastIndex = re.lastIndex
}
elem.add(result, text.slice(lastIndex))
result.normalize()
return result
}
return function() {
var descElem = this.hasBeenSet ? this.p : this.container()
var linkified = elem().add(descElem.textContent).new()
re2preHRef.forEach(function(e) {
;[].filter.call(linkified.childNodes, function(n) {
return n.nodeType === Node.TEXT_NODE
}).forEach(function(n) {
linkified.replaceChild(linkify(n.nodeValue, e[0], e[1]), n)
})
})
removeAllChild(descElem)
descElem.appendChild(linkified)
}
})()
function MovieInfo(movieView, callbacks) {
this.movieView = movieView
this.visible = true
this.toggle = newMovieInfoToggleButton(callbacks.movieInfoToggle)
this.container = elem('div').new()
this.userA = newContributorA()
this.channelA = newContributorA()
this.ngUserButton = newActionButton(callbacks.ngUserButton)
this.ngChannelButton = newActionButton(callbacks.ngChannelButton)
this.tagP = newParagraph()
this.userP = newParagraph('ユーザー: ', this.userA, this.ngUserButton)
this.channelP =
newParagraph('チャンネル: ', this.channelA, this.ngChannelButton)
this.ngUserAction = Action.ADD
this.ngChannelAction = Action.ADD
this.tag2view = Object.create(null)
}
MovieInfo.prototype.setParagraphsDisplay = function(display) {
this.tagP.style.display = display
this.userP.style.display = display
this.channelP.style.display = display
}
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.setParagraphsDisplay(this.visible ? '' : 'none')
}
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()
elem.css(e, { position: 'relative', width: '440px' })
e.appendChild(b)
}
this.setVisible(false)
}
MovieInfo.prototype.removeToggle = function() {
var b = this.toggle
if (b.parentNode) {
var e = this.getItemDataElem()
e.removeChild(b)
elem.css(e, {position: '', width: ''})
}
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, tagButtonCallback) {
var t2v = this.tag2view
var p = this.tagP
tags.forEach(function(tag) {
var a = newTagAnchor(tag)
var b = newActionButton(tagButtonCallback)
elem.add(p, newTagSpan(a, b), ' ')
t2v[tag] = { anchor: a, button: b, action: Action.ADD }
})
this.container.appendChild(p)
this.addContainerToItemContent()
}
MovieInfo.prototype.updateByNgTags = function(ngTags) {
var t2v = this.tag2view
Object.keys(t2v).forEach(function(tag) {
setTagActionAndStyle(t2v[tag], false)
})
ngTags.forEach(function(ngTag) {
setTagActionAndStyle(t2v[ngTag], true)
})
}
MovieInfo.prototype.setContributor = function(contributor, preHRef, a, p) {
a.href = preHRef + contributor.id
a.textContent = contributor.name
this.container.appendChild(p)
this.addContainerToItemContent()
}
MovieInfo.prototype.setUser = function(user) {
this.setContributor(user
, 'http://www.nicovideo.jp/user/'
, this.userA
, this.userP)
}
MovieInfo.prototype.setChannel = function(channel) {
this.setContributor(channel
, 'http://ch.nicovideo.jp/channel/ch'
, this.channelA
, this.channelP)
}
MovieInfo.prototype.updateUserByNg = function(ng) {
this.ngUserAction = ng ? Action.REMOVE : Action.ADD
setContributorStyle(this.userA, this.ngUserButton, ng)
}
MovieInfo.prototype.updateChannelByNg = function(ng) {
this.ngChannelAction = ng ? Action.REMOVE : Action.ADD
setContributorStyle(this.channelA, this.ngChannelButton, ng)
}
function Popup(movieView, callbacks) {
this.movieView = movieView
this.ngIdAction = Action.ADD
this.ngTitleAction = Action.ADD
this.visitAction = Action.ADD
this.ngIdButton = newNgIdButton(callbacks.ngIdButton)
this.ngTitleButton = newNgTitleButton(callbacks.ngTitleButton)
this.visitButton = newVisitButton(callbacks.visitButton)
this.menu = newActionMenu(this.visitButton
, this.ngIdButton
, this.ngTitleButton)
}
Popup.prototype.setNgIdAction = function(action) {
this.ngIdAction = action
this.ngIdButton.textContent = action.ngIdButtonText
}
Popup.prototype.setNgTitleAction = function(action) {
this.ngTitleAction = action
this.ngTitleButton.textContent = action.ngTitleButtonText
}
Popup.prototype.setVisitAction = function(action) {
this.visitAction = action
this.visitButton.textContent = action.visitButtonText
}
Popup.prototype.addToRoot = function(entered, leaved) {
var r = this.movieView.root
r.appendChild(this.menu)
r.addEventListener('mouseenter', entered)
r.addEventListener('mouseleave', leaved)
}
Popup.prototype.setVisible = function(visible) {
this.menu.style.display = visible ? '' : 'none'
}
function Title(a) {
this.a = a
}
Title.prototype.update = function(movie) {
var a = this.a
if (!movie.ngTitle) {
a.textContent = a.textContent
return
}
removeAllChild(a)
var m = movie.matchedNgTitle
var t = movie.title
var i = t.indexOf(m)
if (i !== 0) elem.add(a, t.substring(0, i))
elem.add(a,
elem('span')
.css({'color': 'white', 'background-color': 'fuchsia'})
.add(t.substring(i, i + m.length))
.new())
if (i + m.length !== t.length) elem.add(a, t.substring(i + m.length))
}
Title.prototype.setLineThrough = function(lineThrough) {
this.a.style.textDecoration = lineThrough ? 'line-through' : ''
}
function MovieView(view, titleAnchor, root) {
this.view = view
this.root = root
this.title = new Title(titleAnchor)
this.viewMode = new ViewMode.DoNothing()
this.movieInfo = new MovieInfo(this, view.callbacks)
this.description = new Description(this, view.callbacks)
this.popup = new Popup(this, view.callbacks)
this.errorElem = newErrorElem()
}
MovieView.prototype.setVisible = function(visible) {
this.root.style.display = visible? '' : '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.getMovieViewMode(movie)
if (this.setViewModeIfDiff(m)) this.view.requestLoadingLazyImages()
}
MovieView.prototype.updateVisited = function(movie) {
this.updateViewMode(movie)
this.popup.setVisitAction(movie.visited ? Action.REMOVE : Action.ADD)
}
MovieView.prototype.updateNgId = function(movie) {
this.updateViewMode(movie)
this.title.setLineThrough(movie.ngId)
this.popup.setNgIdAction(movie.ngId ? Action.REMOVE : Action.ADD)
}
MovieView.prototype.updateMatchedNgTitle = function(movie) {
this.title.update(movie)
}
MovieView.prototype.updateNgTitle = function(movie) {
this.updateViewMode(movie)
this.updateMatchedNgTitle(movie)
this.popup.setNgTitleAction(movie.ngTitle ? Action.REMOVE : Action.ADD)
}
MovieView.prototype.updateTags = function(movie) {
this.movieInfo.setTags(movie.tags, this.view.callbacks.tagButton)
if (this.view.model.isMovieInfoTogglable()) {
this.movieInfo.addToggle(movie)
}
}
MovieView.prototype.updateNgTags = function(movie) {
this.updateViewMode(movie)
this.movieInfo.updateByNgTags(movie.ngTags)
}
MovieView.prototype.updateUser = function(movie) {
this.movieInfo.setUser(movie.user)
if (this.view.model.isMovieInfoTogglable()) {
this.movieInfo.addToggle(movie)
}
}
MovieView.prototype.updateNgUser = function(movie) {
this.updateViewMode(movie)
this.movieInfo.updateUserByNg(movie.user.ng)
}
MovieView.prototype.updateChannel = function(movie) {
this.movieInfo.setChannel(movie.channel)
if (this.view.model.isMovieInfoTogglable()) {
this.movieInfo.addToggle(movie)
}
}
MovieView.prototype.updateNgChannel = function(movie) {
this.updateViewMode(movie)
this.movieInfo.updateChannelByNg(movie.channel.ng)
}
MovieView.prototype.updateDescription = function(movie) {
var d = this.description
if (d.isTruncated()) {
d.set(movie.description)
var togglable = this.view.model.isDescriptionTogglable()
d.setExpanded(!togglable)
if (togglable) d.addToggle()
}
d.linkify()
}
MovieView.prototype.updateError = function(movie) {
this.errorElem.textContent = movie.error
this.getMovieDataElem().appendChild(this.errorElem)
}
return MovieView
})()
var GinzaView = (function() {
function radio(value, clickCallback) {
return elem('input')
.attr({
'type': 'radio',
'name': 'visitedMovieViewMode',
'value': value,
})
.on('click', clickCallback)
.new()
}
function movieIdInHRef(href) {
var execResult = /^watch\/([^?]+)/.exec(href)
return execResult ? execResult[1] : null
}
function getTagNameByTagButton(tagButton) {
return tagButton.previousSibling.textContent
}
function getMovieRoot(child) {
for (var e = child; e; e = e.parentNode) {
if (e.className.split(' ').indexOf('item') >= 0) return e
}
return null
}
function getWatchAnchors() {
return array(document.querySelectorAll('p.itemTitle.ranking a'))
}
function getMovieImages() {
return array(document.querySelectorAll('img.thumb'))
}
function getMovieAnchors() {
return getMovieImages().map(function(img) {
return img.parentNode
}).concat(getWatchAnchors())
}
function getMovieIdByRoot(movieRoot) {
return movieRoot.getAttribute('data-id')
}
function getMovieIdByComponent(actionButton) {
return getMovieIdByRoot(getMovieRoot(actionButton))
}
function isInView(img) {
var rect = img.getBoundingClientRect()
return (rect.height && rect.width)
&& ((rect.top >= 0 && rect.top <= window.innerHeight)
|| (rect.bottom >= 0 && rect.bottom <= window.innerHeight)
|| (rect.top < 0 && rect.bottom > window.innerHeight))
}
function checkbox(clickCallback, checked) {
return elem('input')
.attr({type: 'checkbox', checked: checked})
.on('click', clickCallback)
.new()
}
function GinzaView() {
this.ngMovieVisibleCheckbox = checkbox(this.setNgMovieVisible.bind(this))
this.configButton = this.newConfigButton()
this.idToMovieView = Object.create(null)
var radioClickListener = this.setVisitedMovieViewMode.bind(this)
this.reduceRadio = radio('reduce', radioClickListener)
this.hideRadio = radio('hide', radioClickListener)
this.doNothingRadio = radio('doNothing', radioClickListener)
this.callbacks = {
ngUserButton: this.doNgUserId.bind(this),
ngChannelButton: this.doNgChannelId.bind(this),
ngIdButton: this.doNgId.bind(this),
ngTitleButton: this.doNgTitle.bind(this),
visitButton: this.doVisitedId.bind(this),
tagButton: this.doNgTag.bind(this),
movieInfoToggle: this.toggleMovieInfoVisible.bind(this),
descriptionToggle: this.toggleDescriptionExpanded.bind(this),
}
}
GinzaView.prototype.setModel = function(model) {
this.model = model
this.updateVisitedMovieViewMode()
this.updateNewWindowOpen()
this.updateMovieInfoTogglable()
}
GinzaView.prototype.newConfigButton = function() {
return elem('span')
.css({'text-decoration': 'underline', 'cursor': 'pointer'})
.on('click', this.showConfigDialog.bind(this))
.add('設定')
.new()
}
GinzaView.prototype.getMovies = function() {
var result = [], i2v = this.idToMovieView
getWatchAnchors().forEach(function(a) {
var id = movieIdInHRef(a.getAttribute('href'))
var movieView = i2v[id] = new MovieView(this, a, getMovieRoot(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(),
elem('label').add(this.hideRadio, ' 非表示').new(),
elem('label').add(this.doNothingRadio, ' 通常表示').new(),
])
.new()
}
GinzaView.prototype.newControllers = function() {
return elem('div')
.add([
this.newVisitedMovieViewModeFragment(),
' | ',
elem('label').add(this.ngMovieVisibleCheckbox, ' NG動画を表示').new(),
' | ',
this.configButton,
])
.new()
}
GinzaView.prototype.addControllers = function() {
var mainDiv = document.querySelector('div.contentBody.video')
mainDiv.insertBefore(this.newControllers(), mainDiv.firstChild)
}
GinzaView.prototype.movieAnchorClicked = function(event) {
var movieId = getMovieIdByComponent(event.target)
this.model.addVisitedId(movieId)
}
GinzaView.prototype.addAllMovieAnchorListener = function() {
var listener = this.movieAnchorClicked.bind(this)
getMovieAnchors().forEach(function(movieAnchor) {
movieAnchor.addEventListener('click', listener, false)
})
}
GinzaView.prototype.updateNewWindowOpen = function() {
var f = this.model.isNewWindowOpen()
? function(a) { a.setAttribute('target', '_blank') }
: function(a) { a.removeAttribute('target') }
getMovieAnchors().forEach(f)
}
GinzaView.prototype.updateViewMode = function(movie) {
this.idToMovieView[movie.id].updateViewMode(movie)
}
GinzaView.prototype.updateVisitedMovieViewMode = function() {
this.model.getVisitedMovieViewMode().updateView(this)
this.model.movies.forEach(this.updateViewMode, this)
}
GinzaView.prototype.updateNgMovieVisible = function() {
this.ngMovieVisibleCheckbox.checked = this.model.ngMovieVisible
this.model.movies.forEach(this.updateViewMode, this)
}
GinzaView.prototype.getVisitedMovieViewMode = function() {
if (this.doNothingRadio.checked) return new ViewMode.DoNothing()
if (this.hideRadio.checked) return new ViewMode.Hide()
if (this.reduceRadio.checked) return new ViewMode.Reduce()
throw new Error()
}
GinzaView.prototype.addAllActionButton = function() {
var entered = (function(e) {
var v = this.idToMovieView[getMovieIdByRoot(e.target)]
v.popup.setVisible(true)
}).bind(this)
var leaved = (function(e) {
var v = this.idToMovieView[getMovieIdByRoot(e.target)]
v.popup.setVisible(false)
}).bind(this)
this.model.movies.forEach(function(movie) {
this.idToMovieView[movie.id].popup.addToRoot(entered, leaved)
}, this)
}
GinzaView.prototype.toggleDescriptionExpanded = function(event) {
var movieId = getMovieIdByComponent(event.target)
var d = this.idToMovieView[movieId].description
d.setExpanded(!d.expanded)
}
GinzaView.prototype.updateDescriptionTogglable = function() {
var togglable = this.model.isDescriptionTogglable()
var methodName = togglable ? 'addToggle' : 'removeToggle'
var id2view = this.idToMovieView
Object.keys(id2view).map(function(key) {
return id2view[key]
}).filter(function(view) {
return view.description.hasBeenSet
}).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 = getMovieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.movieInfo.setVisible(!v.movieInfo.visible)
}
GinzaView.prototype.updateMovieInfoTogglable = function() {
var f = this.model.isMovieInfoTogglable()
? this.addMovieInfoToggleButton
: this.removeMovieInfoToggleButton
this.model.movies.filter(function(movie) {
return movie.hasExternalInfo()
}).forEach(f, this)
}
GinzaView.prototype.setVisitedMovieViewMode = function() {
if (this.reduceRadio.checked)
this.model.setVisitedMovieViewMode(new ViewMode.Reduce())
else if (this.hideRadio.checked)
this.model.setVisitedMovieViewMode(new ViewMode.Hide())
else if (this.doNothingRadio.checked)
this.model.setVisitedMovieViewMode(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.doVisitedId = function(event) {
var movieId = getMovieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.popup.visitAction.doVisitedId(this.model, movieId)
}
GinzaView.prototype.doNgId = function(event) {
var movieId = getMovieIdByComponent(event.target)
this.idToMovieView[movieId].popup.ngIdAction.doNgId(this.model, movieId)
}
GinzaView.prototype.doNgTitle = function(event) {
var movieId = getMovieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.popup.ngTitleAction.doNgTitle(this.model, movieId)
}
GinzaView.prototype.doNgTag = function(event) {
var movieId = getMovieIdByComponent(event.target)
var tag = getTagNameByTagButton(event.target)
var movieView = this.idToMovieView[movieId]
movieView.movieInfo.tag2view[tag].action.doNgTag(this.model, tag)
}
GinzaView.prototype.doNgUserId = function(event) {
var movieId = getMovieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.movieInfo.ngUserAction.doNgUserId(this.model, movieId)
}
GinzaView.prototype.doNgChannelId = function(event) {
var movieId = getMovieIdByComponent(event.target)
var v = this.idToMovieView[movieId]
v.movieInfo.ngChannelAction.doNgChannelId(this.model, movieId)
}
GinzaView.prototype.loadLazyImages = function() {
var imgs = document.querySelectorAll('img.thumb.jsLazyImage')
Array.prototype.forEach.call(imgs, function(img) {
if (isInView(img)) {
img.src = img.getAttribute('data-original')
img.setAttribute('data-original', '')
img.classList.remove('jsLazyImage')
}
})
}
GinzaView.prototype.requestLoadingLazyImages = function() {
if (this.loadingLazyImagesRequested) return
this.loadingLazyImagesRequested = true
setTimeout((function() {
this.loadLazyImages()
this.loadingLazyImagesRequested = false
}).bind(this), 0)
}
return GinzaView
})()
var ConfigDialog = (function() {
function opt(val, text, selected) {
return elem('option').attr({value: val, selected: selected}).add(text)
}
function action(targetOption) {
function noEmptyStr(v) { return v !== '' }
function isPositiveInt(v) {
var i = parseInt(v)
return !isNaN(i) && i > 0
}
function toInt(v) { return parseInt(v) }
var errMessage = '1以上の整数を入力して下さい。\n'
return {
'ng-movie-id': {
get: function(model) { return model.getNgIds() },
add: function(model, val) { return model.addNgId(val) },
remove: function(model, vals) { model.removeNgId(vals) },
removeAll: function(model) { model.clearNgIds() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/watch/' + v },
},
'ng-title': {
get: function(model) { return model.getNgTitles() },
add: function(model, val) { return model.addNgTitle(val) },
remove: function(model, vals) { model.removeNgTitles(vals) },
removeAll: function(model) { model.clearNgTitles() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/search/' + v },
},
'ng-tag': {
get: function(model) { return model.getNgTags() },
add: function(model, val) { return model.addNgTag(val) },
remove: function(model, vals) { model.removeNgTags(vals) },
removeAll: function(model) { model.clearNgTags() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/tag/' + v },
},
'ng-user-id': {
get: function(model) { return model.getNgUserIds() },
add: function(model, val) {
return model.addNgUserId(parseInt(val))
},
remove: function(model, vals) {
model.removeNgUserId(vals.map(toInt))
},
removeAll: function(model) { model.clearNgUserIds() },
valid: isPositiveInt,
errMessage: errMessage,
url: function(v) { return 'http://www.nicovideo.jp/user/' + v },
},
'ng-channel-id': {
get: function(model) { return model.getNgChannelIds() },
add: function(model, val) {
return model.addNgChannelId(parseInt(val))
},
remove: function(model, vals) {
model.removeNgChannelId(vals.map(toInt))
},
removeAll: function(model) { model.clearNgChannelIds() },
valid: isPositiveInt,
errMessage: errMessage,
url: function(v) { return 'http://ch.nicovideo.jp/ch' + v },
},
'visited-movie-id': {
get: function(model) { return model.getVisitedIds() },
add: function(model, val) { return model.addVisitedId(val) },
remove: function(model, vals) { model.removeVisitedId(vals) },
removeAll: function(model) { model.clearVisitedIds() },
valid: noEmptyStr,
errMessage: '',
url: function(v) { return 'http://www.nicovideo.jp/watch/' + v },
},
}[targetOption.value]
}
var buttonStyle = {
display: 'block',
width: '100%',
'margin-bottom': '5px',
}
function setupDoc(doc) {
elem.css(doc.body, { width: '20em', margin: '8px' })
return doc
}
function newBackground() {
return elem('div')
.css({
'background-color': 'black',
opacity: '0.5',
'z-index': ConfigDialog.Z_INDEX - 1,
position: 'fixed',
top: '0px',
left: '0px',
width: '100%',
height: '100%',
})
.new()
}
function checkbox(name) {
return elem('input')
.attr({ type: 'checkbox', checked: this.model['is' + name]() })
.on('change', this['update' + name].bind(this))
.new(this.doc)
}
function label(control, text) {
return elem('label').css('display', 'block').add(control, text)
}
function ConfigDialog(model, doc) {
this.model = model
this.doc = setupDoc(doc)
this.background = newBackground()
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')
this.useGetThumbInfoCheckbox = checkbox.call(this, 'UseGetThumbInfo')
this.movieInfoTogglableCheckbox = checkbox.call(this
, 'MovieInfoTogglable')
this.descriptionTogglableCheckbox = checkbox.call(this
, '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.getNgTitles().map(function(t) {
return elem('option').add(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(elem('option').add(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.text }))
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.text))
})
}
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(elem('option').add(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.setNewWindowOpen(this.newWindowOpenCheckbox.checked)
}
ConfigDialog.prototype.updateUseGetThumbInfo = function() {
this.model.setUseGetThumbInfo(this.useGetThumbInfoCheckbox.checked)
}
ConfigDialog.prototype.updateMovieInfoTogglable = function() {
this.model.setMovieInfoTogglable(this.movieInfoTogglableCheckbox.checked)
}
ConfigDialog.prototype.updateDescriptionTogglable = function() {
this.model.setDescriptionTogglable(
this.descriptionTogglableCheckbox.checked)
}
return ConfigDialog
})()
function main() {
var view = new GinzaView()
var model = new Model(view.getMovies(), view)
view.setModel(model)
view.addControllers()
view.addAllActionButton()
view.addAllMovieAnchorListener()
model.updateAllVisited()
model.updateAllNgIds()
model.updateAllNgTitles()
window.addEventListener('scroll', function() {
view.loadLazyImages()
}, false)
view.model.requestGetThumbInfo()
}
main()
})()