Nico Nico Ranking NG

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

当前为 2014-06-08 提交的版本,查看 最新版本

// ==UserScript==
// @name Nico Nico Ranking NG
// @namespace http://userscripts.org/users/121129 
// @description ニコニコ動画のランキングにNG機能を追加
// @match http://www.nicovideo.jp/ranking*
// @version 7
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @license MIT License
// ==/UserScript==

;(function() {
  'use strict'

  function removeAllChild(parent) {
    while (parent.firstChild) parent.removeChild(parent.firstChild)
  }
  function elem(tagName, attrMap, styleMap) {
    var result = document.createElement(tagName)
    for (var attrName in attrMap)
      if (attrMap.hasOwnProperty(attrName))
        result.setAttribute(attrName, attrMap[attrName])
    for (var styleName in styleMap)
      if (styleMap.hasOwnProperty(styleName))
        result.style.setProperty(styleName, styleMap[styleName], null)
    for (var i = 3, n = arguments.length; i < n; i++) {
      var arg = arguments[i]
      if (typeof(arg) === 'string')
        result.appendChild(document.createTextNode(arg))
      else
        result.appendChild(arg)
    }
    return result
  }
  function label(element, text) {
    return elem('label', {}, {}, element, text)
  }
  function array(likeArray) {
    return Array.prototype.slice.call(likeArray)
  }
  function checkbox(clickListener, checked) {
    var result = elem('input', { type: 'checkbox' })
    result.checked = checked
    result.addEventListener('click', clickListener, false)
    return result
  }
  
  var Movie = (function() {
    function Movie(id, title, view) {
      this.id = id
      this.title = title
      this.visited = false
      this.ngId = false
      this.ngTitle = false
      this.matchedNgTitle = ''
      this.tags = []
      this.ngTags = []
      this.userId = -1
      this.ngUser = false
      this.view = view
    }
    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.ngUser
    }
    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.setUserId = function(userId) {
      if (this.userId !== userId) {
        this.userId = userId
        this.view.updateUserId(this)
      }
    }
    Movie.prototype.setNgUserByNgUserIds = function(ngUserIds) {
      var ngUser = (ngUserIds.indexOf(this.userId) !== -1)
      if (this.ngUser !== ngUser) {
        this.ngUser = ngUser
        this.view.updateNgUser(this)
      }
    }
    return Movie
  })()

  var Model = (function() {
    function Model(movies, view) {
      this.movies = movies
      this.view = view
      this.ngMovieVisible = false
      this.idToMovie = newIdToMovieMap(movies)
    }
    function newIdToMovieMap(movies) {
      var result = {}
      movies.forEach(function(movie) { result[movie.id] = movie })
      return result
    }
    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()
      }
    }
    function GM_getArray(name) {
      return JSON.parse(GM_getValue(name, '[]'))
    }
    function indexOfRegExp(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 (indexOfRegExp(values, value)) return false
      values.push(value)
      GM_setValue(name, JSON.stringify(values))
      return true
    }
    function GM_removeFromArray(name, value) {
      var newValue = GM_getArray(name).filter(function(element) {
        return element !== value
      })
      GM_setValue(name, JSON.stringify(newValue))
    }
    Model.prototype.getVisitedIds = function() {
      return GM_getArray('visitedMovies')
    }
    Model.prototype.addVisitedId = function(movieId) {
      GM_addToArray('visitedMovies', movieId)
      if (this.idToMovie[movieId]) this.idToMovie[movieId].setVisited(true)
    }
    Model.prototype.removeVisitedId = function(movieId) {
      GM_removeFromArray('visitedMovies', movieId)
      if (this.idToMovie[movieId]) this.idToMovie[movieId].setVisited(false)
    }
    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) {
      GM_addToArray('ngMovies', movieId)
      if (this.idToMovie[movieId]) this.idToMovie[movieId].setNgId(true)
    }
    Model.prototype.removeNgId = function(movieId) {
      GM_removeFromArray('ngMovies', movieId)
      if (this.idToMovie[movieId]) this.idToMovie[movieId].setNgId(false)
    }
    Model.prototype.clearNgIds = function() {
      GM_deleteValue('ngMovies')
      this.movies.forEach(function(movie) { movie.setNgId(false) })
    }
    Model.prototype.getNgTitles = function() { return GM_getArray('ngTitles') }
    function escapeNoWordChars(s) { return String(s).replace(/\W/g, '\\$&') }
    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) {
      GM_addToArray('ngUserIds', ngUserId)
      this.updateAllNgUser()
    }
    Model.prototype.removeNgUserId = function(ngUserId) {
      GM_removeFromArray('ngUserIds', ngUserId)
      this.updateAllNgUser()
    }
    Model.prototype.clearNgUserIds = function() {
      GM_deleteValue('ngUserIds')
      this.updateAllNgUser()
    }
    function newPattern(elements) {
      var s = '('
      elements.forEach(function(e) { s += escapeNoWordChars(e) + '|' })
      return s.slice(0, -1) + ')'
    }
    function newStrictPattern(elements) {
      return '^' + newPattern(elements) + '$'
    }
    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()
        , 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()
        , 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.setNgUserByNgUserIds(ngUserIds)
      })
    }
    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()
    }
    function evaluateTags(doc, movie) {
      var r = doc.evaluate('/nicovideo_thumb_response/thumb/tags/tag'
                         , doc
                         , null
                         , XPathResult.ORDERED_NODE_ITERATOR_TYPE
                         , null)
      var tags = []
      for (var tag; tag = r.iterateNext();) tags.push(tag.textContent)
      movie.setTags(tags)
    }
    function evaluateUserId(doc, movie) {
      var r = doc.evaluate('/nicovideo_thumb_response/thumb/user_id/text()'
                         , doc
                         , null
                         , XPathResult.STRING_TYPE
                         , null)
      var userId = parseInt(r.stringValue, 10)
      if (!isNaN(userId)) movie.setUserId(userId)
    }
    Model.prototype.evalResAndRequestNext = function(movies, movie, res) {
      var d = new DOMParser().parseFromString(res.responseText
                                            , 'application/xml')
      evaluateTags(d, movie)
      evaluateUserId(d, movie)
      
      var ngTags = this.getNgTags()
      if (ngTags.length)
        movie.setNgTagsByRegExp(new RegExp(newStrictPattern(ngTags), 'i'))
      
      movie.setNgUserByNgUserIds(this.getNgUserIds())
      
      this.requestNext(movies)
    }
    Model.prototype.requestNext = function(movies) {
      var nextMovie = movies.shift()
      if (nextMovie) {
        GM_xmlhttpRequest(
          { method: 'GET'
          , url: 'http://ext.nicovideo.jp/api/getthumbinfo/' + nextMovie.id
          , onload : this.evalResAndRequestNext.bind(this, movies, nextMovie)
          })
      }
    }
    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.requestNext(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タイトルを入力'
          var 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].userId)
      }
    }
  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].userId)
      }
    }
  
  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(movieId, view) {}
  ViewMode.DoNothing.prototype.setViewMode = function(movieId, view) {}
  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(movieId, view) {
    view.setMovieVisible(movieId, true)
  }
  ViewMode.Hide.prototype.setViewMode = function(movieId, view) {
    view.setMovieVisible(movieId, 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 setTagAndUserIdDisplay(movieView, display) {
      movieView.tagP.style.display = display
      movieView.userIdP.style.display = display
    }
    
    function Reduce() {}
    Reduce.prototype = new ViewMode()
    Reduce.prototype.name = 'reduce'
    Reduce.prototype.restoreViewMode = function(movieId, view) {
      var o = { display: ''
              , rankingNumFontSize: ''
              , videoListWrapWidth: ''
              , itemTitleWidth: ''
              , itemThumb: { width: '', height: '' }
              }
      var r = view.getMovieRootById(movieId)
      setStyle(r, o)
      this.restoreThumb(r.querySelector('.thumb'))
      setTagAndUserIdDisplay(view.idToMovieView[movieId], '')
      view.updateMovieInfoVisible(movieId)
    }
    Reduce.prototype.setViewMode = function(movieId, view) {
      var o = { display: 'none'
              , rankingNumFontSize: '150%'
              , videoListWrapWidth: '80px'
              , itemTitleWidth: 'auto'
              , itemThumb: { width: '80px', height: '45px' }
              }
      var r = view.getMovieRootById(movieId)
      setStyle(r, o)
      this.halfThumb(r.querySelector('.thumb'))
      setTagAndUserIdDisplay(view.idToMovieView[movieId], '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 GinzaView = (function() {
    function radio(value, clickListener) {
      var result = elem('input',
                        { 'type': 'radio'
                        , 'name': 'visitedMovieViewMode'
                        , 'value': value
                        })
      result.addEventListener('click', clickListener, false)
      return result
    }
    function anchor(text, clickListener) {
      var result = elem('a'
                      , { 'href': 'javascript:void(0)'
                        , 'style': 'color:#FFF;'
                        }
                      , {}
                      , text)
      if (clickListener) result.addEventListener('click', clickListener, false)
      return result
    }
    function movieIdInHRef(href) {
      var execResult = /^watch\/([^?]+)/.exec(href)
      return execResult ? execResult[1] : null
    }
    function addSeparator(elem, separator) {
      elem.appendChild(document.createTextNode(separator || ' | '))
    }
    var decodeHtmlCharRef = (function() {
      var e = elem('span')
      return function(text) {
        e.innerHTML = text
        return e.textContent
      }
    })()
    function getTagNameByTagButton(tagButton) {
      return tagButton.previousSibling.textContent
    }
    
    function MovieView(view) {
      this.viewMode = new ViewMode.DoNothing()
      this.ngIdAction = Action.ADD
      this.ngTitleAction = Action.ADD
      this.visitType = Action.ADD
      this.ngIdButton = view.newNgIdButton()
      this.ngTitleButton = view.newNgTitleButton()
      this.visitButton = view.newVisitButton()
      this.tagToView = {}
      this.ngUserAction = Action.ADD
      this.ngUserButton = view.newNgUserButton()
      this.userIdAnchor = elem('a')
      this.movieInfoToggleButton = view.newToggleButton()
      this.tagP = view.newParagraph()
      this.userIdP = view.newParagraph()
      this.movieInfoVisible = false
    }
    
    function GinzaView() {
      this.ngMovieVisibleCheckbox = checkbox(this.setNgMovieVisible.bind(this))
      this.configButton = this.newConfigButton()
      this.idToMovieView = {}
      
      var radioClickListener = this.setVisitedMovieViewMode.bind(this)
      this.reduceRadio = radio('reduce', radioClickListener)
      this.hideRadio = radio('hide', radioClickListener)
      this.doNothingRadio = radio('doNothing', radioClickListener)
    }
    GinzaView.prototype.setModel = function(model) {
      this.model = model
      
      var idToMovieView = this.idToMovieView
      model.movies.forEach(function(movie) {
        idToMovieView[movie.id] = new MovieView(this)
      }, this)
      
      this.updateVisitedMovieViewMode()
      this.updateNewWindowOpen()
      this.updateMovieInfoTogglable()
    }
    GinzaView.prototype.newConfigButton = function() {
      var result =  elem('span'
                       , {}
                       ,{ 'text-decoration': 'underline', 'cursor': 'pointer'}
                       , '設定')
      result.addEventListener('click', this.showConfigDialog.bind(this), false)
      return result
    }
    GinzaView.prototype.newNgUserButton = function() {
      var result = elem('span', {}, { cursor: 'pointer' }, '[+]')
      result.addEventListener('click'
                            , this.ngUserButtonListener
                              || (this.ngUserButtonListener =
                                    this.doNgUserId.bind(this))
                            , false)
      return result
    }
    GinzaView.prototype.newNgIdButton = function() {
      return anchor(Action.ADD.ngIdButtonText
                  , this.ngIdButtonListener
                    || (this.ngIdButtonListener = this.doNgId.bind(this)))
    }
    GinzaView.prototype.newNgTitleButton = function() {
      return anchor(Action.ADD.ngTitleButtonText
                  , this.ngTitleButtonListener
                    || (this.ngTitleButtonListener =
                          this.doNgTitle.bind(this)))
    }
    GinzaView.prototype.newVisitButton = function() {
      return anchor(Action.ADD.visitButtonText
                  , this.visitButtonListener
                    || (this.visitButtonListener =
                          this.doVisitedId.bind(this)))
    }
    GinzaView.prototype.getMovies = function() {
      return this.getWatchAnchors().map(function(a) {
        return new Movie(movieIdInHRef(a.getAttribute('href'))
                                     , a.textContent
                                     , this)
      }, this)
    }
    GinzaView.prototype.newVisitedMovieViewModeFragment = function() {
      var result = document.createDocumentFragment()
      result.appendChild(document.createTextNode('閲覧済みの動画を'))
      result.appendChild(label(this.reduceRadio, ' 縮小'))
      result.appendChild(label(this.hideRadio, ' 非表示'))
      result.appendChild(label(this.doNothingRadio, ' 通常表示'))
      return result
    }
    GinzaView.prototype.newControllers = function() {
      var result = document.createElement('div')
      result.appendChild(this.newVisitedMovieViewModeFragment())
      addSeparator(result)
      result.appendChild(label(this.ngMovieVisibleCheckbox, ' NG動画を表示'))
      addSeparator(result)
      result.appendChild(this.configButton)
      return result
    }
    GinzaView.prototype.addControllers = function() {
      var mainDiv = document.querySelector('div.contentBody.video')
      mainDiv.insertBefore(this.newControllers(), mainDiv.firstChild)
    }
    GinzaView.prototype.getMovieRoot = function(child) {
      return this.getItemElem(child)
    }
    GinzaView.prototype.getMovieRootById = function(movieId) {
      return this.getMovieRoot(this.getWatchAnchor(movieId))
    }
    GinzaView.prototype.getWatchAnchors = function() {
      return this.watchAnchors
          || (this.watchAnchors =
               array(document.querySelectorAll('p.itemTitle.ranking a')))
    }
    GinzaView.prototype.newIdToWatchAnchorMap = function() {
      var result = {}
      this.getWatchAnchors().forEach(function(anchor) {
        result[movieIdInHRef(anchor.getAttribute('href'))] = anchor
      })
      return result
    }
    GinzaView.prototype.getWatchAnchor = function(movieId) {
      var m = this.idToWatchAnchor
      return (m && m[movieId])
          || (this.idToWatchAnchor = this.newIdToWatchAnchorMap())[movieId]
    }
    GinzaView.prototype.getMovieAnchors = function() {
      return this.movieAnchors
          || (this.movieAnchors = this.getMovieImages().map(function(img) {
               return img.parentNode
             }).concat(this.getWatchAnchors()))
    }
    GinzaView.prototype.setMovieVisible = function(movieId, visible) {
      var r = this.getMovieRootById(movieId)
      if (visible) r.style.removeProperty('display')
      else r.style.display = 'none'
    }
    GinzaView.prototype.movieAnchorClicked = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      this.model.addVisitedId(movieId)
    }
    GinzaView.prototype.addAllMovieAnchorListener = function() {
      var listener = this.movieAnchorClicked.bind(this)
      this.getMovieAnchors().forEach(function(movieAnchor) {
        movieAnchor.addEventListener('click', listener, false)
      })
    }
    GinzaView.prototype.updateNewWindowOpen = function() {
      this.getMovieAnchors().forEach(function(movieAnchor) {
        if (this.model.isNewWindowOpen())
          movieAnchor.setAttribute('target', '_blank')
        else
          movieAnchor.removeAttribute('target')
      }, this)
    }
    GinzaView.prototype.updateViewMode = function(movie) {
      var movieView = this.idToMovieView[movie.id]
      var oldViewMode = movieView.viewMode
      var newViewMode = this.model.getMovieViewMode(movie)
      if (oldViewMode.hasSameName(newViewMode)) return
      oldViewMode.restoreViewMode(movie.id, this)
      newViewMode.setViewMode(movie.id, this)
      movieView.viewMode = newViewMode
      this.requestLoadingLazyImages()
    }
    GinzaView.prototype.updateVisitedMovieViewMode = function() {
      this.model.getVisitedMovieViewMode().updateView(this)
      this.model.movies.forEach(function(m) { this.updateViewMode(m) }, this)
    }
    GinzaView.prototype.updateNgMovieVisible = function() {
      this.ngMovieVisibleCheckbox.checked = this.model.ngMovieVisible
      this.model.movies.forEach(function(m) { this.updateViewMode(m) }, this)
    }
    GinzaView.prototype.updateNgId = function(movie) {
      this.updateViewMode(movie)
      if (movie.ngId)
        this.getWatchAnchor(movie.id).style.textDecoration = 'line-through'
      else
        this.getWatchAnchor(movie.id).style.removeProperty('text-decoration')
      var action = movie.ngId ? Action.REMOVE : Action.ADD
      var movieView = this.idToMovieView[movie.id]
      movieView.ngIdAction = action
      movieView.ngIdButton.textContent = action.ngIdButtonText
    }
    GinzaView.prototype.updateMatchedNgTitle = function(movie) {
      var a = this.getWatchAnchor(movie.id)
      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) a.appendChild(document.createTextNode(t.substring(0, i)))
      a.appendChild(elem('span'
                       , {}
                       , { 'color': 'white', 'background-color': 'fuchsia' }
                       , t.substring(i, i + m.length)))
      if (i + m.length !== t.length)
        a.appendChild(document.createTextNode(t.substring(i + m.length)))
    }
    GinzaView.prototype.updateNgTitle = function(movie) {
      this.updateViewMode(movie)
      this.updateMatchedNgTitle(movie)
      var movieView = this.idToMovieView[movie.id]
      var action = movie.ngTitle ? Action.REMOVE : Action.ADD
      movieView.ngTitleAction = action
      movieView.ngTitleButton.textContent = action.ngTitleButtonText
    }
    GinzaView.prototype.getItemContent = function(movieId) {
      return this.getWatchAnchor(movieId).parentNode.parentNode
    }
    GinzaView.prototype.updateTags = function(movie) {
      var movieView = this.idToMovieView[movie.id]
      var tagToView = movieView.tagToView
      
      var tagList = movieView.tagP
      if (movieView.viewMode.isReduceMode()) tagList.style.display = 'none'
      
      var listener = this.tagButtonListener
      if (!listener)
        listener = this.tagButtonListener = this.doNgTag.bind(this)
      
      movie.tags.forEach(function(tag) {
        var anchor = elem('a'
                        , { href: 'http://www.nicovideo.jp/tag/' + tag }
                        , { color: this.movieInfoAnchorColor }
                        , decodeHtmlCharRef(tag))
        
        var button = elem('span', {}, { cursor: 'pointer' }, '[+]')
        button.addEventListener('click', listener, false)
        
        tagList.appendChild(elem('span'
                               , {}
                               , { 'white-space': 'nowrap'
                                 , 'margin-right': '0.5em'
                                 }
                               , anchor
                               , button))
        tagList.appendChild(document.createTextNode(' '))
        
        tagToView[tag] = { anchor: anchor, button: button, action: Action.ADD }
      }, this)
  
      this.getItemContent(movie.id).appendChild(tagList)
      if (this.model.isMovieInfoTogglable())
        this.addMovieInfoToggleButton(movie)
    }
    GinzaView.prototype.setTagStyle = function(tagView, ng) {
      tagView.anchor.style.color = ng ? 'white' : this.movieInfoAnchorColor
      tagView.anchor.style.backgroundColor = ng ? 'fuchsia' : ''
      tagView.button.textContent = ng ? '[x]' : '[+]'
      tagView.action = ng ? Action.REMOVE : Action.ADD
    }
    GinzaView.prototype.updateNgTags = function(movie) {
      this.updateViewMode(movie)
      var tagToView = this.idToMovieView[movie.id].tagToView
      movie.tags.forEach(function(tag) {
        this.setTagStyle(tagToView[tag], false)
      }, this)
      movie.ngTags.forEach(function(ngTag) {
        this.setTagStyle(tagToView[ngTag], true)
      }, this)
    }
    GinzaView.prototype.updateVisited = function(movie) {
      this.updateViewMode(movie)
      var movieView = this.idToMovieView[movie.id]
      var action = movie.visited ? Action.REMOVE : Action.ADD
      movieView.visitType = action
      movieView.visitButton.textContent = action.visitButtonText
    }
    GinzaView.prototype.getMovieIdByComponent = function(actionButton) {
      return this.getMovieIdByRoot(this.getMovieRoot(actionButton))
    }
    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.getItemElem = function(child) {
      for (var e = child; e; e = e.parentNode) {
        if (e.className.split(' ').indexOf('item') >= 0) return e
      }
      return null
    }
    GinzaView.prototype.addAllActionButton = function() {
      var entered = (function(e) {
        var v = this.idToMovieView[this.getMovieIdByRoot(e.target)]
        v.visitButton.parentNode.style.display = ''
      }).bind(this)
      var leaved = (function(e) {
        var v = this.idToMovieView[this.getMovieIdByRoot(e.target)]
        v.visitButton.parentNode.style.display = 'none'
      }).bind(this)
      this.model.movies.forEach(function(movie) {
        var movieView = this.idToMovieView[movie.id]
        var menu = document.createElement('div')
        menu.style.position = 'absolute'
        menu.style.top = '10px'
        menu.style.right = '0px'
        menu.style.padding = '3px'
        menu.style.color = '#999'
        menu.style.backgroundColor = 'rgb(105, 105, 105)'
        menu.style.display = 'none'
        menu.appendChild(movieView.visitButton)
        menu.appendChild(document.createTextNode(' | '))
        menu.appendChild(movieView.ngIdButton)
        menu.appendChild(document.createTextNode(' | '))
        menu.appendChild(movieView.ngTitleButton)
        var root = this.getMovieRootById(movie.id)
        root.appendChild(menu)
        root.addEventListener('mouseenter', entered, false)
        root.addEventListener('mouseleave', leaved, false)
      }, this)
    }
    GinzaView.prototype.getMovieImages = function() {
      var result = this.movieImages
      if (!result) {
        var imgs = document.querySelectorAll('img.thumb')
        this.movieImages = result = array(imgs)
      }
      return result
    }
    GinzaView.prototype.getMovieDataElem = function(movieId) {
      return this.getMovieRootById(movieId).querySelector('ul.list')
    }
    GinzaView.prototype.newToggleButton = function() {
      var result = document.createElement('li')
      result.className = 'count'
      result.textContent = '▼'
      result.style.cursor = 'pointer'
      result.style.marginLeft = '10px'
      result.addEventListener('click'
                            , this.toggleButtonListener
                              || (this.toggleButtonListener =
                                    this.toggleMovieInfoVisible.bind(this))
                            , false)
      return result
    }
    GinzaView.prototype.getMovieIdByRoot = function(movieRoot) {
      return movieRoot.getAttribute('data-id')
    }
    GinzaView.prototype.movieInfoAnchorColor = '#333333'
    GinzaView.prototype.newParagraph = function() {
      return elem('p'
                , { 'class': 'font12' }
                , { 'margin-top': '4px', 'line-height': '1.5em' })
    }
    GinzaView.prototype.updateUserId = function(movie) {
      var v = this.idToMovieView[movie.id]
      var anchor = v.userIdAnchor
      anchor.href = 'http://www.nicovideo.jp/user/' + movie.userId
      anchor.textContent = movie.userId
      anchor.style.color = this.movieInfoAnchorColor
      var userId = v.userIdP
      userId.appendChild(document.createTextNode('ユーザーID: '))
      userId.appendChild(anchor)
      userId.appendChild(v.ngUserButton)
      if (v.viewMode.isReduceMode()) userId.style.display = 'none'
      this.getItemContent(movie.id).appendChild(userId)
      if (this.model.isMovieInfoTogglable())
        this.addMovieInfoToggleButton(movie)
    }
    GinzaView.prototype.updateNgUser = function(movie) {
      this.updateViewMode(movie)
      var v = this.idToMovieView[movie.id]
      var ng = movie.ngUser
      v.userIdAnchor.style.color = ng ? 'white' : this.movieInfoAnchorColor
      v.userIdAnchor.style.backgroundColor = ng ? 'fuchsia' : ''
      v.ngUserButton.textContent = ng ? '[x]' : '[+]'
      v.ngUserAction = ng ? Action.REMOVE : Action.ADD
    }
    GinzaView.prototype.addMovieInfoToggleButton = function(movie) {
      var v = this.idToMovieView[movie.id]
      v.movieInfoVisible = false
      var button = v.movieInfoToggleButton
      if (!button.parentNode && (movie.tags.length || movie.userId !== -1))
        this.getMovieDataElem(movie.id).appendChild(button)
      this.updateMovieInfoVisible(movie.id)
    }
    GinzaView.prototype.removeMovieInfoToggleButton = function(movie) {
      var v = this.idToMovieView[movie.id]
      v.movieInfoVisible = true
      this.updateMovieInfoVisible(movie.id)
      var button = v.movieInfoToggleButton
      if (button.parentNode) button.parentNode.removeChild(button)
      var viewMode = v.viewMode
      if (viewMode.isReduceMode()) {
        viewMode.restoreViewMode(movie.id, this)
        viewMode.setViewMode(movie.id, this)
      }
    }
    GinzaView.prototype.toggleMovieInfoVisible = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      var v = this.idToMovieView[movieId]
      v.movieInfoVisible = !v.movieInfoVisible
      this.updateMovieInfoVisible(movieId)
    }
    GinzaView.prototype.updateMovieInfoVisible = function(movieId) {
      var v = this.idToMovieView[movieId]
      var visible = v.movieInfoVisible
      v.movieInfoToggleButton.textContent = visible ? '▲' : '▼'
      v.tagP.style.display = visible ? '' : 'none'
      v.userIdP.style.display = visible ? '' : 'none'
    }
    GinzaView.prototype.updateMovieInfoTogglable = function() {
      if (this.model.isMovieInfoTogglable())
        this.model.movies.forEach(this.addMovieInfoToggleButton, this)
      else
        this.model.movies.forEach(this.removeMovieInfoToggleButton, 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() {
      new ConfigDialog(this.model).show()
    }
    GinzaView.prototype.doVisitedId = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      this.idToMovieView[movieId].visitType.doVisitedId(this.model, movieId)
    }
    GinzaView.prototype.doNgId = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      this.idToMovieView[movieId].ngIdAction.doNgId(this.model, movieId)
    }
    GinzaView.prototype.doNgTitle = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      this.idToMovieView[movieId].ngTitleAction.doNgTitle(this.model, movieId)
    }
    GinzaView.prototype.doNgTag = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      var tag = getTagNameByTagButton(event.target)
      var movieView = this.idToMovieView[movieId]
      movieView.tagToView[tag].action.doNgTag(this.model, tag)
    }
    GinzaView.prototype.doNgUserId = function(event) {
      var movieId = this.getMovieIdByComponent(event.target)
      this.idToMovieView[movieId].ngUserAction.doNgUserId(this.model, movieId)
    }
    GinzaView.prototype.isInView = function(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))
    }
    GinzaView.prototype.loadLazyImages = function() {
      var imgs = document.querySelectorAll('img.thumb.jsLazyImage')
      Array.prototype.forEach.call(imgs, function(img) {
        if (this.isInView(img)) {
          img.src = img.getAttribute('data-original')
          img.setAttribute('data-original', '')
          img.className = img.className.split(' ').filter(function(name) {
            return name !== 'jsLazyImage'
          }).join(' ')
        }
      }, this)
    }
    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 ConfigDialog(model) {
      this.model = model
      this.background = new DialogBackground(ConfigDialog.Z_INDEX - 1)
      
      this.ngTitleLabel = this.newNgTitleLabel()
      this.ngTitleSelect = this.newNgTitleSelect()
      this.ngTitleAddButton = newAddButton(this.addNgTitle.bind(this))
      this.ngTitleRemoveButton = this.newNgTitleRemoveButton()
      
      this.ngTagLabel = tabLabel('NGタグ', this.selectNgTagTab.bind(this))
      this.ngTagSelect = this.newNgTagSelect()
      this.ngTagAddButton = newAddButton(this.addNgTag.bind(this), 'none')
      this.ngTagRemoveButton = this.newNgTagRemoveButton()
      
      this.allNgIdRemoveButton = this.newAllNgIdRemoveButton()
      this.allNgTitleRemoveButton = this.newAllNgTitleRemoveButton()
      this.allVisitedIdRemoveButton = this.newAllVisitedIdRemoveButton()
      this.allNgTagRemoveButton = this.newAllNgTagRemoveButton()
      this.allNgUserIdRemoveButton = this.newAllNgUserIdRemoveButton()
      
      this.newWindowOpenCheckbox = this.newNewWindowOpenCheckbox()
      this.useGetThumbInfoCheckbox = this.newUseGetThumbInfoCheckbox()
      this.movieInfoTogglableCheckbox = this.newMovieInfoTogglableCheckbox()
      
      this.root = this.newRoot()
    }
    ConfigDialog.Z_INDEX = 10000
    function tabLabel(text, clickListener, styleMap) {
      var style = { 'margin-right': '5px'
                  , cursor: 'pointer'
                  , display: 'inline-block'
                  , padding: '3px'
                  }
      for (var p in styleMap)
        if (styleMap.hasOwnProperty(p)) style[p] = styleMap[p]
      var result = elem('span', {}, style, text)
      result.addEventListener('click', clickListener, false)
      return result
    }
    ConfigDialog.prototype.newNgTitleLabel = function() {
      return tabLabel('NGタイトル'
                    , this.selectNgTitleTab.bind(this)
                    , { 'background-color': 'gray', color: 'white' })
    }
    function select(optionTexts, changeListener, display) {
      var styleMap = { 'width': '250px'
                     , 'float': 'left'
                     , 'margin-bottom': '10px'
                     , 'margin-top': '0px'
                     }
      if (display) styleMap['display'] = display
      var result = elem('select'
                      , { size: '10', multiple: 'multiple' }
                      , styleMap)
      optionTexts.forEach(function(optionText) {
        result.appendChild(elem('option', {}, {}, optionText))
      })
      result.addEventListener('change', changeListener, false)
      return result
    }
    ConfigDialog.prototype.newNgTitleSelect = function() {
      return select(this.model.getNgTitles()
                  , this.updateNgTitleRemoveButtonDisabled.bind(this))
    }
    ConfigDialog.prototype.newNgTagSelect = function() {
      return select(this.model.getNgTags()
                  , this.updateNgTagRemoveButtonDisabled.bind(this)
                  , 'none')
    }
    ConfigDialog.prototype.newNewWindowOpenCheckbox = function() {
      return checkbox(this.updateNewWindowOpen.bind(this)
                    , this.model.isNewWindowOpen())
    }
    ConfigDialog.prototype.newUseGetThumbInfoCheckbox = function() {
      return checkbox(this.updateUseGetThumbInfo.bind(this)
                    , this.model.isUseGetThumbInfo())
    }
    ConfigDialog.prototype.newMovieInfoTogglableCheckbox = function() {
      return checkbox(this.updateMovieInfoTogglable.bind(this)
                    , this.model.isMovieInfoTogglable())
    }
    function button(text, clickListener, styleMap, disabled) {
      var result = elem('button', {}, styleMap, text)
      result.addEventListener('click', clickListener, false)
      result.disabled = disabled
      return result
    }
    function setWidth(styleMap) {
      styleMap['width'] = '70px'
      return styleMap
    }
    function newAddButton(clickListener, display) {
      var style = {}
      if (display) style['display'] = display
      return button('追加', clickListener, setWidth(style))
    }
    function newRemoveButton(clickListener, display) {
      var style = { 'margin-top': '5px' }
      if (display) style['display'] = display
      return button('削除', clickListener, setWidth(style), true)
    }
    ConfigDialog.prototype.newNgTitleRemoveButton = function() {
      return newRemoveButton(this.removeSelectedNgTitles.bind(this))
    }
    ConfigDialog.prototype.newNgTagRemoveButton = function() {
      return newRemoveButton(this.removeSelectedNgTags.bind(this), 'none')
    }
    ConfigDialog.prototype.newAllNgTitleRemoveButton = function() {
      return button('NGタイトルをすべて削除'
                  , this.removeAllNgTitle.bind(this)
                  , {}
                  , !this.model.getNgTitles().length)
    }
    ConfigDialog.prototype.newAllNgTagRemoveButton = function() {
      return button('NGタグをすべて削除'
                  , this.removeAllNgTag.bind(this)
                  , {}
                  , !this.model.getNgTags().length)
    }
    ConfigDialog.prototype.newAllNgIdRemoveButton = function() {
      return button('NG登録をすべて削除'
                  , this.removeAllNgId.bind(this)
                  , {}
                  , !this.model.getNgIds().length)
    }
    ConfigDialog.prototype.newAllVisitedIdRemoveButton = function() {
      return button('訪問履歴をすべて削除'
                  , this.removeAllVisitedId.bind(this)
                  , {}
                  , !this.model.getVisitedIds().length)
    }
    ConfigDialog.prototype.newAllNgUserIdRemoveButton = function() {
      return button('NGユーザーをすべて削除'
                  , this.removeAllNgUserId.bind(this)
                  , {}
                  , !this.model.getNgUserIds().length)
    }
    function marginTopDiv(child, styleMap) {
      var style = { 'margin-top': '10px' }
      for (var p in styleMap)
        if (styleMap.hasOwnProperty(p)) style[p] = styleMap[p]
      return elem('div', {}, style, child)
    }
    ConfigDialog.prototype.newRoot = function() {
      return elem('div'
                , {}
                , { 'background-color': 'white'
                  , 'z-index': String(ConfigDialog.Z_INDEX)
                  , 'position': 'fixed'
                  , 'padding': '15px'
                  , 'border': 'medium solid black'
                  , 'overflow': 'auto'
                  , 'width': '320px'
                  }
                , elem('div', {}, {}, this.ngTitleLabel, this.ngTagLabel)
                , this.ngTitleSelect
                , this.ngTitleAddButton
                , this.ngTitleRemoveButton
                , this.ngTagSelect
                , this.ngTagAddButton
                , this.ngTagRemoveButton
                , marginTopDiv(this.allNgTitleRemoveButton, { clear: 'left' })
                , marginTopDiv(this.allNgIdRemoveButton)
                , marginTopDiv(this.allVisitedIdRemoveButton)
                , marginTopDiv(this.allNgTagRemoveButton)
                , marginTopDiv(this.allNgUserIdRemoveButton)
                , marginTopDiv(label(this.newWindowOpenCheckbox
                                   , ' 動画を別窓で開く'))
                , marginTopDiv(label(this.useGetThumbInfoCheckbox
                                   , ' 動画情報を取得する'))
                , marginTopDiv(label(this.movieInfoTogglableCheckbox
                                   , ' 動画情報の表示切替ボタンを使用する'))
                , marginTopDiv(button('閉じる', this.hide.bind(this))
                             , { 'text-align': 'center' }))
    }
    ConfigDialog.prototype.show = function(owner) {
      this.background.show()
      
      var r = this.root
      document.body.appendChild(r)
      r.style.left = ((window.innerWidth - r.offsetWidth) / 2) + 'px'
      r.style.top = ((window.innerHeight - r.offsetHeight) / 2) + 'px'
    }
    ConfigDialog.prototype.hide = function() {
      this.root.parentNode.removeChild(this.root)
      this.background.hide()
    }
    ConfigDialog.prototype.removeAllVisitedId = function() {
      this.model.clearVisitedIds()
      this.allVisitedIdRemoveButton.disabled = true
    }
    ConfigDialog.prototype.removeAllNgId = function() {
      this.model.clearNgIds()
      this.allNgIdRemoveButton.disabled = true
    }
    ConfigDialog.prototype.removeAllNgTitle = function() {
      this.model.clearNgTitles()
      removeAllChild(this.ngTitleSelect)
      this.allNgTitleRemoveButton.disabled = true
      this.updateNgTitleRemoveButtonDisabled()
    }
    ConfigDialog.prototype.removeAllNgUserId = function() {
      this.model.clearNgUserIds()
      this.allNgUserIdRemoveButton.disabled = true
    }
    function selectedOptions(select) {
      return array(select.options).filter(function(o) { return o.selected })
    }
    function removeSelectedItems(select
                               , removeItems
                               , updateButtonDisabled
                               , removeAllButton) {
      var options = selectedOptions(select)
      removeItems(options.map(function(o) { return o.textContent }), false)
      options.forEach(function(o) { o.parentNode.removeChild(o) })
      updateButtonDisabled()
      if (!select.options.length) removeAllButton.disabled = true
    }
    ConfigDialog.prototype.removeSelectedNgTitles = function() {
      removeSelectedItems(this.ngTitleSelect
                        , this.model.removeNgTitles.bind(this.model)
                        , this.updateNgTitleRemoveButtonDisabled.bind(this)
                        , this.allNgTitleRemoveButton)
    }
    ConfigDialog.prototype.updateNewWindowOpen = function() {
      this.model.setNewWindowOpen(this.newWindowOpenCheckbox.checked)
    }
    ConfigDialog.prototype.updateUseGetThumbInfo = function() {
      this.model.setUseGetThumbInfo(this.useGetThumbInfoCheckbox.checked)
    }
    ConfigDialog.prototype.updateNgTitleRemoveButtonDisabled = function() {
      this.ngTitleRemoveButton.disabled =
        this.ngTitleSelect.selectedIndex === -1
    }
    ConfigDialog.prototype.removeSelectedNgTags = function() {
      removeSelectedItems(this.ngTagSelect
                        , this.model.removeNgTags.bind(this.model)
                        , this.updateNgTagRemoveButtonDisabled.bind(this)
                        , this.allNgTagRemoveButton)
    }
    ConfigDialog.prototype.removeAllNgTag = function() {
      this.model.clearNgTags()
      removeAllChild(this.ngTagSelect)
      this.allNgTagRemoveButton.disabled = true
      this.updateNgTagRemoveButtonDisabled()
    }
    ConfigDialog.prototype.updateNgTagRemoveButtonDisabled = function() {
      this.ngTagRemoveButton.disabled =
        this.ngTagSelect.selectedIndex === -1
    }
    ConfigDialog.prototype.getNgTitleObj = function() {
      return { label: this.ngTitleLabel
             , select: this.ngTitleSelect
             , addButton: this.ngTitleAddButton
             , removeButton: this.ngTitleRemoveButton
             }
    }
    ConfigDialog.prototype.getNgTabObj = function() {
      return { label: this.ngTagLabel
             , select: this.ngTagSelect
             , addButton: this.ngTagAddButton
             , removeButton: this.ngTagRemoveButton
             }
    }
    function setTabState(obj, selected) {
      obj.label.style.backgroundColor = selected ? 'gray' : ''
      obj.label.style.color = selected ? 'white' : ''
      ;[obj.select, obj.addButton, obj.removeButton].forEach(function(e) {
        e.style.display = selected ? '' : 'none'
      })
    }
    ConfigDialog.prototype.selectNgTitleTab = function() {
      setTabState(this.getNgTitleObj(), true)
      setTabState(this.getNgTabObj(), false)
    }
    ConfigDialog.prototype.selectNgTagTab = function() {
      setTabState(this.getNgTitleObj(), false)
      setTabState(this.getNgTabObj(), true)
    }
    ConfigDialog.prototype.addNgTitle = function() {
      var r = null
      do {
        r = window.prompt((r ? '"' + r + '"は登録済みです。\n' : '')
                        + 'NGタイトルを入力')
        if (!r) return
      } while (!this.model.addNgTitle(r))
      this.ngTitleSelect.appendChild(elem('option', {}, {}, r))
      this.allNgTitleRemoveButton.disabled = false
    }
    ConfigDialog.prototype.addNgTag = function() {
      var r = null
      do {
        r = window.prompt((r ? '"' + r + '"は登録済みです。\n' : '')
                        + 'NGタグを入力')
        if (!r) return
      } while (!this.model.addNgTag(r))
      this.ngTagSelect.appendChild(elem('option', {}, {}, r))
      this.allNgTagRemoveButton.disabled = false
    }
    ConfigDialog.prototype.updateMovieInfoTogglable = function() {
      this.model.setMovieInfoTogglable(this.movieInfoTogglableCheckbox.checked)
    }
    return ConfigDialog
  })()

  function DialogBackground(zIndex) {
    this.root = elem('div'
                    , {}
                    , { 'background-color': 'black'
                      , 'opacity': '0.5'
                      , 'z-index': String(zIndex)
                      , 'position': 'fixed'
                      , 'left': '0px'
                      , 'top': '0px'
                      , 'width': '100%'
                      , 'height': '100%'
                      })
  }
  DialogBackground.prototype.show = function() {
    document.body.appendChild(this.root)
  }
  DialogBackground.prototype.hide = function() {
    this.root.parentNode.removeChild(this.root)
  }

  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或关注我们的公众号极客氢云获取最新地址