Greasy Fork镜像 还支持 简体中文。

Nico Nico Ranking NG

ニコニコ動画のランキングにNG機能を追加

目前為 2014-10-31 提交的版本,檢視 最新版本

// ==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()
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址