Greasy Fork is available in English.

Nico Nico Ranking NG

ニコニコ動画のランキングとキーワード・タグ検索結果に NG 機能を追加

  1. // ==UserScript==
  2. // @name Nico Nico Ranking NG
  3. // @namespace http://userscripts.org/users/121129
  4. // @description ニコニコ動画のランキングとキーワード・タグ検索結果に NG 機能を追加
  5. // @match *://www.nicovideo.jp/ranking/genre/*
  6. // @match *://www.nicovideo.jp/ranking/hot_topic*
  7. // @match *://www.nicovideo.jp/search/*
  8. // @match *://www.nicovideo.jp/tag/*
  9. // @version 59
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_openInTab
  14. // @grant GM.getValue
  15. // @grant GM.setValue
  16. // @grant GM.xmlHttpRequest
  17. // @grant GM.openInTab
  18. // @license MIT License
  19. // @noframes
  20. // @run-at document-start
  21. // @connect ext.nicovideo.jp
  22. // ==/UserScript==
  23.  
  24. // https://d3js.org/d3-dsv/ Version 1.0.0. Copyright 2016 Mike Bostock.
  25. ;(function (global, factory) {
  26. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  27. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  28. (factory((global.d3 = global.d3 || {})));
  29. }(this, function (exports) { 'use strict';
  30.  
  31. function objectConverter(columns) {
  32. return new Function("d", "return {" + columns.map(function(name, i) {
  33. return JSON.stringify(name) + ": d[" + i + "]";
  34. }).join(",") + "}");
  35. }
  36.  
  37. function customConverter(columns, f) {
  38. var object = objectConverter(columns);
  39. return function(row, i) {
  40. return f(object(row), i, columns);
  41. };
  42. }
  43.  
  44. // Compute unique columns in order of discovery.
  45. function inferColumns(rows) {
  46. var columnSet = Object.create(null),
  47. columns = [];
  48.  
  49. rows.forEach(function(row) {
  50. for (var column in row) {
  51. if (!(column in columnSet)) {
  52. columns.push(columnSet[column] = column);
  53. }
  54. }
  55. });
  56.  
  57. return columns;
  58. }
  59.  
  60. function dsv(delimiter) {
  61. var reFormat = new RegExp("[\"" + delimiter + "\n]"),
  62. delimiterCode = delimiter.charCodeAt(0);
  63.  
  64. function parse(text, f) {
  65. var convert, columns, rows = parseRows(text, function(row, i) {
  66. if (convert) return convert(row, i - 1);
  67. columns = row, convert = f ? customConverter(row, f) : objectConverter(row);
  68. });
  69. rows.columns = columns;
  70. return rows;
  71. }
  72.  
  73. function parseRows(text, f) {
  74. var EOL = {}, // sentinel value for end-of-line
  75. EOF = {}, // sentinel value for end-of-file
  76. rows = [], // output rows
  77. N = text.length,
  78. I = 0, // current character index
  79. n = 0, // the current line number
  80. t, // the current token
  81. eol; // is the current token followed by EOL?
  82.  
  83. function token() {
  84. if (I >= N) return EOF; // special case: end of file
  85. if (eol) return eol = false, EOL; // special case: end of line
  86.  
  87. // special case: quotes
  88. var j = I, c;
  89. if (text.charCodeAt(j) === 34) {
  90. var i = j;
  91. while (i++ < N) {
  92. if (text.charCodeAt(i) === 34) {
  93. if (text.charCodeAt(i + 1) !== 34) break;
  94. ++i;
  95. }
  96. }
  97. I = i + 2;
  98. c = text.charCodeAt(i + 1);
  99. if (c === 13) {
  100. eol = true;
  101. if (text.charCodeAt(i + 2) === 10) ++I;
  102. } else if (c === 10) {
  103. eol = true;
  104. }
  105. return text.slice(j + 1, i).replace(/""/g, "\"");
  106. }
  107.  
  108. // common case: find next delimiter or newline
  109. while (I < N) {
  110. var k = 1;
  111. c = text.charCodeAt(I++);
  112. if (c === 10) eol = true; // \n
  113. else if (c === 13) { eol = true; if (text.charCodeAt(I) === 10) ++I, ++k; } // \r|\r\n
  114. else if (c !== delimiterCode) continue;
  115. return text.slice(j, I - k);
  116. }
  117.  
  118. // special case: last token before EOF
  119. return text.slice(j);
  120. }
  121.  
  122. while ((t = token()) !== EOF) {
  123. var a = [];
  124. while (t !== EOL && t !== EOF) {
  125. a.push(t);
  126. t = token();
  127. }
  128. if (f && (a = f(a, n++)) == null) continue;
  129. rows.push(a);
  130. }
  131.  
  132. return rows;
  133. }
  134.  
  135. function format(rows, columns) {
  136. if (columns == null) columns = inferColumns(rows);
  137. return [columns.map(formatValue).join(delimiter)].concat(rows.map(function(row) {
  138. return columns.map(function(column) {
  139. return formatValue(row[column]);
  140. }).join(delimiter);
  141. })).join("\n");
  142. }
  143.  
  144. function formatRows(rows) {
  145. return rows.map(formatRow).join("\n");
  146. }
  147.  
  148. function formatRow(row) {
  149. return row.map(formatValue).join(delimiter);
  150. }
  151.  
  152. function formatValue(text) {
  153. return text == null ? ""
  154. : reFormat.test(text += "") ? "\"" + text.replace(/"/g, "\"\"") + "\""
  155. : text;
  156. }
  157.  
  158. return {
  159. parse: parse,
  160. parseRows: parseRows,
  161. format: format,
  162. formatRows: formatRows
  163. };
  164. }
  165.  
  166. var csv = dsv(",");
  167.  
  168. var csvParse = csv.parse;
  169. var csvParseRows = csv.parseRows;
  170. var csvFormat = csv.format;
  171. var csvFormatRows = csv.formatRows;
  172.  
  173. var tsv = dsv("\t");
  174.  
  175. var tsvParse = tsv.parse;
  176. var tsvParseRows = tsv.parseRows;
  177. var tsvFormat = tsv.format;
  178. var tsvFormatRows = tsv.formatRows;
  179.  
  180. exports.dsvFormat = dsv;
  181. exports.csvParse = csvParse;
  182. exports.csvParseRows = csvParseRows;
  183. exports.csvFormat = csvFormat;
  184. exports.csvFormatRows = csvFormatRows;
  185. exports.tsvParse = tsvParse;
  186. exports.tsvParseRows = tsvParseRows;
  187. exports.tsvFormat = tsvFormat;
  188. exports.tsvFormatRows = tsvFormatRows;
  189.  
  190. Object.defineProperty(exports, '__esModule', { value: true });
  191.  
  192. }));
  193.  
  194. ;(function() {
  195. 'use strict'
  196.  
  197. var createObject = function(prototype, properties) {
  198. var descriptors = function() {
  199. return Object.keys(properties).reduce(function(descriptors, key) {
  200. descriptors[key] = Object.getOwnPropertyDescriptor(properties, key)
  201. return descriptors
  202. }, {})
  203. }
  204. return Object.defineProperties(Object.create(prototype), descriptors())
  205. }
  206. var set = function(target, propertyName) {
  207. return function(value) { target[propertyName] = value }
  208. }
  209. var movieIdOf = function(absoluteMovieURL) {
  210. return new URL(absoluteMovieURL).pathname.slice('/watch/'.length)
  211. }
  212. const ancestor = (child, selector) => {
  213. for (let n = child.parentNode; n; n = n.parentNode)
  214. if (n.matches(selector))
  215. return n
  216. return null
  217. }
  218.  
  219. var EventEmitter = (function() {
  220. var EventEmitter = function() {
  221. this._eventNameToListeners = new Map()
  222. }
  223. EventEmitter.prototype = {
  224. on(eventName, listener) {
  225. var m = this._eventNameToListeners
  226. var v = m.get(eventName)
  227. if (v) {
  228. v.add(listener)
  229. } else {
  230. m.set(eventName, new Set([listener]))
  231. }
  232. return this
  233. },
  234. emit(eventName) {
  235. var m = this._eventNameToListeners
  236. var args = Array.from(arguments).slice(1)
  237. for (var l of m.get(eventName) || []) l(...args)
  238. },
  239. off(eventName, listener) {
  240. var v = this._eventNameToListeners.get(eventName)
  241. if (v) v.delete(listener)
  242. },
  243. }
  244. return EventEmitter
  245. })()
  246.  
  247. var Listeners = (function() {
  248. var Listeners = function(eventNameToListener) {
  249. this.eventNameToListener = eventNameToListener
  250. this.eventEmitter = null
  251. }
  252. Listeners.prototype = {
  253. bind(eventEmitter) {
  254. this.eventEmitter = eventEmitter
  255. Object.keys(this.eventNameToListener).forEach(function(k) {
  256. eventEmitter.on(k, this.eventNameToListener[k])
  257. }, this)
  258. },
  259. unbind() {
  260. if (!this.eventEmitter) return
  261. Object.keys(this.eventNameToListener).forEach(function(k) {
  262. this.eventEmitter.off(k, this.eventNameToListener[k])
  263. }, this)
  264. },
  265. }
  266. return Listeners
  267. })()
  268.  
  269. var ArrayStore = (function(_super) {
  270. var isObject = function(v) {
  271. return v === Object(v)
  272. }
  273. var valueIfObj = function(v) {
  274. return isObject(v) ? v.value : v
  275. }
  276. var toUpperCase = function(s) {
  277. return s.toUpperCase()
  278. }
  279. var ArrayStore = function(getValue, setValue, key, caseInsensitive) {
  280. _super.call(this)
  281. this.getValue = getValue
  282. this.setValue = setValue
  283. this.key = key
  284. this.caseInsensitive = Boolean(caseInsensitive)
  285. this._arrayWithText = []
  286. }
  287. ArrayStore.prototype = createObject(_super.prototype, {
  288. get array() {
  289. return this.arrayWithText.map(valueIfObj)
  290. },
  291. get arrayWithText() {
  292. return this._arrayWithText
  293. },
  294. _setOf(values) {
  295. return new Set(this.caseInsensitive ? values.map(toUpperCase) : values)
  296. },
  297. get set() {
  298. return this._setOf(this.array)
  299. },
  300. _toUpperCaseIfRequired(value) {
  301. return this.caseInsensitive ? value.toUpperCase() : value
  302. },
  303. _concat(value, text) {
  304. return this.arrayWithText.concat(text ? {value, text} : value)
  305. },
  306. add(value, text) {
  307. if (this.set.has(this._toUpperCaseIfRequired(value))) return false
  308. this.arrayWithText.push(text ? {value, text} : value)
  309. this.setValue(this.key, JSON.stringify(this.arrayWithText))
  310. this.emit('changed', this.set)
  311. return true
  312. },
  313. async addAsync(value, text) {
  314. await this.sync()
  315. return this.add(value, text)
  316. },
  317. addAll(values) {
  318. if (values.length === 0) return
  319. var oldVals = this.arrayWithText
  320. var set = this._setOf(oldVals.map(valueIfObj))
  321. var filtered = values.filter(function(v) {
  322. return !set.has(this._toUpperCaseIfRequired(valueIfObj(v)))
  323. }, this)
  324. if (filtered.length === 0) return
  325. this.arrayWithText.push(...filtered)
  326. this.setValue(this.key, JSON.stringify(this.arrayWithText))
  327. this.emit('changed', this.set)
  328. },
  329. _reject(values) {
  330. var valueSet = this._setOf(values)
  331. return this.arrayWithText.filter(function(v) {
  332. return !valueSet.has(this._toUpperCaseIfRequired(valueIfObj(v)))
  333. }, this)
  334. },
  335. remove(values) {
  336. const oldVals = this.arrayWithText
  337. const newVals = this._reject(values)
  338. if (oldVals.length === newVals.length) return
  339. this._arrayWithText = newVals
  340. this.setValue(this.key, JSON.stringify(newVals))
  341. this.emit('changed', this.set)
  342. },
  343. async removeAsync(values) {
  344. await this.sync()
  345. this.remove(values)
  346. },
  347. clear() {
  348. if (!this.arrayWithText.length) return
  349. this._arrayWithText = []
  350. this.setValue(this.key, '[]')
  351. this.emit('changed', new Set())
  352. },
  353. async sync() {
  354. this._arrayWithText = JSON.parse(await this.getValue(this.key, '[]'))
  355. },
  356. })
  357. return ArrayStore
  358. })(EventEmitter)
  359.  
  360. var Store = (function(_super) {
  361. var Store = function(getValue, setValue, key, defaultValue) {
  362. _super.call(this)
  363. this.getValue = getValue
  364. this.setValue = setValue
  365. this.key = key
  366. this._value = this.defaultValue = defaultValue
  367. }
  368. Store.prototype = createObject(_super.prototype, {
  369. get value() {
  370. return this._value
  371. },
  372. set value(value) {
  373. if (this._value === value) return
  374. this._value = value
  375. this.setValue(this.key, value)
  376. this.emit('changed', value)
  377. },
  378. async sync() {
  379. this._value = await this.getValue(this.key, this.defaultValue)
  380. }
  381. })
  382. return Store
  383. })(EventEmitter)
  384.  
  385. var Config = (function() {
  386. var ngMovieVisibleStore = function() {
  387. var value
  388. var getValue = function(_, defval) {
  389. return value === undefined ? defval : value
  390. }
  391. var setValue = function(_, v) { value = v }
  392. return new Store(getValue, setValue, 'ngMovieVisible', false)
  393. }
  394. var csv = (function() {
  395. var RECORD_LENGTH = 3
  396. var TYPE = 0
  397. var VALUE = 1
  398. var TEXT = 2
  399. var isObject = function(v) {
  400. return v === Object(v)
  401. }
  402. var csvToArray = function(csv) {
  403. /*
  404. パース対象の文字列最後の文字がカンマのとき、
  405. そのカンマが空のフィールドとしてパースされない。
  406. \n を追加して対処する。
  407. */
  408. return d3.csvParseRows(csv + '\n')
  409. }
  410. var createRecord = function(type, value, text) {
  411. var result = []
  412. result[TYPE] = type
  413. result[VALUE] = value
  414. result[TEXT] = text
  415. return result
  416. }
  417. var trimFields = function(record) {
  418. var r = record
  419. return createRecord(r[TYPE].trim(), r[VALUE].trim(), r[TEXT].trim())
  420. }
  421. var isIntValueType = function(type) {
  422. return ['ngUserId', 'ngChannelId'].indexOf(type) >= 0
  423. }
  424. var hasValidValue = function(record) {
  425. var v = record[VALUE]
  426. return v.length !== 0
  427. && !(isIntValueType(record[TYPE]) && Number.isNaN(Math.trunc(v)))
  428. }
  429. var valueToIntIfIntValueType = function(record) {
  430. var r = record
  431. return isIntValueType(r[TYPE])
  432. ? createRecord(r[TYPE], Math.trunc(r[VALUE]), r[TEXT])
  433. : r
  434. }
  435. var records = function(csv) {
  436. return csvToArray(csv)
  437. .filter(function(record) { return record.length === RECORD_LENGTH })
  438. .map(trimFields)
  439. .filter(hasValidValue)
  440. .map(valueToIntIfIntValueType)
  441. }
  442. var isValueOnlyType = function(type) {
  443. return ['ngTitle', 'ngTag', 'ngUserName'].indexOf(type) >= 0
  444. }
  445. var getValue = function(record) {
  446. var value = record[VALUE]
  447. if (isValueOnlyType(record[TYPE])) return value
  448. var text = record[TEXT]
  449. return text ? {value, text} : value
  450. }
  451. var createTypeToValuesMap = function() {
  452. return new Map([
  453. ['ngMovieId', []],
  454. ['ngTitle', []],
  455. ['ngTag', []],
  456. ['ngUserId', []],
  457. ['ngUserName', []],
  458. ['ngChannelId', []],
  459. ['visitedMovieId', []],
  460. ])
  461. }
  462. return {
  463. create(arrayStore, type) {
  464. return d3.csvFormatRows(arrayStore.arrayWithText.map(function(value) {
  465. return isObject(value)
  466. ? createRecord(type, value.value, value.text)
  467. : createRecord(type, value, '')
  468. }))
  469. },
  470. parse(csv) {
  471. var result = createTypeToValuesMap()
  472. for (var r of records(csv)) {
  473. var values = result.get(r[TYPE])
  474. if (values) values.push(getValue(r))
  475. }
  476. return result
  477. },
  478. }
  479. })()
  480.  
  481. var Config = function(getValue, setValue) {
  482. var store = function(key, defaultValue) {
  483. return new Store(getValue, setValue, key, defaultValue)
  484. }
  485. var arrayStore = function(key, caseInsensitive) {
  486. return new ArrayStore(getValue, setValue, key, caseInsensitive)
  487. }
  488. this.visitedMovieViewMode = store('visitedMovieViewMode', 'reduce')
  489. this.visibleContributorType = store('visibleContributorType', 'all')
  490. this.openNewWindow = store('openNewWindow', true)
  491. this.useGetThumbInfo = store('useGetThumbInfo', true)
  492. this.movieInfoTogglable = store('movieInfoTogglable', true)
  493. this.descriptionTogglable = store('descriptionTogglable', true)
  494. this.visitedMovies = arrayStore('visitedMovies')
  495. this.ngMovies = arrayStore('ngMovies')
  496. this.ngTitles = arrayStore('ngTitles', true)
  497. this.ngTags = arrayStore('ngTags', true)
  498. this.ngLockedTags = arrayStore('ngLockedTags', true)
  499. this.ngUserIds = arrayStore('ngUserIds')
  500. this.ngUserNames = arrayStore('ngUserNames', true)
  501. this.ngChannelIds = arrayStore('ngChannelIds')
  502. this.ngMovieVisible = ngMovieVisibleStore()
  503. this.addToNgLockedTags = store('addToNgLockedTags', false);
  504. }
  505. Config.prototype.sync = function() {
  506. return Promise.all([
  507. this.visitedMovieViewMode.sync(),
  508. this.visibleContributorType.sync(),
  509. this.openNewWindow.sync(),
  510. this.useGetThumbInfo.sync(),
  511. this.movieInfoTogglable.sync(),
  512. this.descriptionTogglable.sync(),
  513. this.visitedMovies.sync(),
  514. this.ngMovies.sync(),
  515. this.ngTitles.sync(),
  516. this.ngTags.sync(),
  517. this.ngLockedTags.sync(),
  518. this.ngUserIds.sync(),
  519. this.ngUserNames.sync(),
  520. this.ngChannelIds.sync(),
  521. this.addToNgLockedTags.sync(),
  522. ])
  523. }
  524. Config.prototype.toCSV = async function(targetTypes) {
  525. await this.sync()
  526. var csvTexts = []
  527. if (targetTypes['ngMovieId']) {
  528. csvTexts.push(csv.create(this.ngMovies, 'ngMovieId'))
  529. }
  530. if (targetTypes['ngTitle']) {
  531. csvTexts.push(csv.create(this.ngTitles, 'ngTitle'))
  532. }
  533. if (targetTypes['ngTag']) {
  534. csvTexts.push(csv.create(this.ngTags, 'ngTag'))
  535. }
  536. if (targetTypes['ngUserId']) {
  537. csvTexts.push(csv.create(this.ngUserIds, 'ngUserId'))
  538. }
  539. if (targetTypes['ngUserName']) {
  540. csvTexts.push(csv.create(this.ngUserNames, 'ngUserName'))
  541. }
  542. if (targetTypes['ngChannelId']) {
  543. csvTexts.push(csv.create(this.ngChannelIds, 'ngChannelId'))
  544. }
  545. if (targetTypes['visitedMovieId']) {
  546. csvTexts.push(csv.create(this.visitedMovies, 'visitedMovieId'))
  547. }
  548. return csvTexts.filter(Boolean).join('\n')
  549. }
  550. Config.prototype.addFromCSV = async function(csvText) {
  551. await this.sync()
  552. var map = csv.parse(csvText)
  553. this.ngMovies.addAll(map.get('ngMovieId'))
  554. this.ngTitles.addAll(map.get('ngTitle'))
  555. this.ngTags.addAll(map.get('ngTag'))
  556. this.ngUserIds.addAll(map.get('ngUserId'))
  557. this.ngUserNames.addAll(map.get('ngUserName'))
  558. this.ngChannelIds.addAll(map.get('ngChannelId'))
  559. this.visitedMovies.addAll(map.get('visitedMovieId'))
  560. }
  561. return Config
  562. })()
  563.  
  564. var ThumbInfo = (function(_super) {
  565. const parseTags = tags => {
  566. return Array.from(tags, tag => {
  567. return {
  568. name: tag.textContent,
  569. lock: tag.getAttribute('lock') === '1',
  570. };
  571. });
  572. };
  573. var contributor = function(rootElem, type, id, name) {
  574. return {
  575. type: type,
  576. id: parseInt(rootElem.querySelector(id).textContent),
  577. name: rootElem.querySelector(name)?.textContent ?? '',
  578. }
  579. }
  580. var user = function(rootElem) {
  581. return contributor(rootElem
  582. , 'user'
  583. , 'thumb > user_id'
  584. , 'thumb > user_nickname')
  585. }
  586. var channel = function(rootElem) {
  587. return contributor(rootElem
  588. , 'channel'
  589. , 'thumb > ch_id'
  590. , 'thumb > ch_name')
  591. }
  592. var parseContributor = function(rootElem) {
  593. var userId = rootElem.querySelector('thumb > user_id')
  594. return userId ? user(rootElem) : channel(rootElem)
  595. }
  596. var parseThumbInfo = function(rootElem) {
  597. return {
  598. description: rootElem.querySelector('thumb > description').textContent,
  599. tags: parseTags(rootElem.querySelectorAll('thumb > tags > tag')),
  600. contributor: parseContributor(rootElem),
  601. title: rootElem.querySelector('thumb > title').textContent,
  602. error: {type: 'NO_ERROR', message: 'no error'},
  603. }
  604. }
  605. var error = function(type, message, id) {
  606. var result = {error: {type, message}}
  607. if (id) result.id = id
  608. return result
  609. }
  610. var parseError = function(rootElem) {
  611. var type = rootElem.querySelector('error > code').textContent
  612. switch (type) {
  613. case 'DELETED': return error(type, '削除された動画')
  614. case 'NOT_FOUND': return error(type, '見つからない、または無効な動画')
  615. case 'COMMUNITY': return error(type, 'コミュニティ限定動画')
  616. default: return error(type, 'エラーコード: ' + type)
  617. }
  618. }
  619. var parseResText = function(resText) {
  620. try {
  621. var d = new DOMParser().parseFromString(resText, 'application/xml')
  622. var r = d.documentElement
  623. var status = r.getAttribute('status')
  624. switch (status) {
  625. case 'ok': return parseThumbInfo(r)
  626. case 'fail': return parseError(r)
  627. default: return error(status, 'ステータス: ' + status)
  628. }
  629. } catch (e) {
  630. return error('PARSING', 'パースエラー')
  631. }
  632. }
  633. var statusMessage = function(res) {
  634. return res.status + ' ' + res.statusText
  635. }
  636.  
  637. var ThumbInfo = function(httpRequest, concurrent) {
  638. _super.call(this)
  639. this.httpRequest = httpRequest
  640. this.concurrent = concurrent || 5
  641. this._requestCount = 0
  642. this._pendingIds = []
  643. this._requestedIds = new Set()
  644. }
  645. ThumbInfo.prototype = createObject(_super.prototype, {
  646. _onerror(id) {
  647. this._requestCount--
  648. this._requestNextMovie()
  649. this.emit('errorOccurred', error('ERROR', 'エラー', id))
  650. },
  651. _ontimeout(id, retried) {
  652. if (retried) {
  653. this._requestCount--
  654. this._requestNextMovie()
  655. this.emit('errorOccurred', error('TIMEOUT', 'タイムアウト', id))
  656. } else {
  657. this._requestMovie(id, true)
  658. }
  659. },
  660. _onload(id, res) {
  661. this._requestCount--
  662. this._requestNextMovie()
  663. if (res.status === 200) {
  664. var thumbInfo = parseResText(res.responseText)
  665. thumbInfo.id = id
  666. if (thumbInfo.error.type === 'NO_ERROR') {
  667. this.emit('completed', thumbInfo)
  668. } else {
  669. this.emit('errorOccurred', thumbInfo)
  670. }
  671. } else {
  672. this.emit('errorOccurred'
  673. , error('HTTP_STATUS', statusMessage(res), id))
  674. }
  675. },
  676. _requestMovie(id, retry) {
  677. this.httpRequest({
  678. method: 'GET',
  679. url: 'https://ext.nicovideo.jp/api/getthumbinfo/' + id,
  680. timeout: 5000,
  681. onload: this._onload.bind(this, id),
  682. onerror: this._onerror.bind(this, id),
  683. ontimeout: this._ontimeout.bind(this, id, retry),
  684. })
  685. },
  686. _requestNextMovie() {
  687. var id = this._pendingIds.shift()
  688. if (!id) return
  689. this._requestMovie(id)
  690. this._requestCount++
  691. },
  692. _getNewIds(ids) {
  693. ids = ids || []
  694. var m = this._requestedIds
  695. return [...new Set(ids)].filter(function(id) { return !m.has(id) })
  696. },
  697. _requestAsPossible() {
  698. var space = this.concurrent - this._requestCount
  699. var c = Math.min(this._pendingIds.length, space)
  700. for (var i = 0; i < c; i++) this._requestNextMovie()
  701. },
  702. request(ids, prefer) {
  703. const newIds = this._getNewIds(ids)
  704. for (const id of newIds) this._requestedIds.add(id)
  705. if (prefer) {
  706. for (const id of newIds) this._requestMovie(id)
  707. return
  708. }
  709. ;[].push.apply(this._pendingIds, newIds)
  710. this._requestAsPossible()
  711. return this
  712. },
  713. })
  714. return ThumbInfo
  715. })(EventEmitter)
  716.  
  717. var Tag = (function(_super) {
  718. var Tag = function(thumbInfoTabObj) {
  719. _super.call(this);
  720. this.name = thumbInfoTabObj.name;
  721. this.lock = thumbInfoTabObj.lock;
  722. this.ngByNormal = false;
  723. this.ngByLock = false;
  724. }
  725. Tag.prototype = createObject(_super.prototype, {
  726. get ng() {
  727. return this.ngByNormal || this.ngByLock;
  728. },
  729. updateNg(upperCaseNgTagNameSet) {
  730. var pre = this.ng
  731. this.ngByNormal = upperCaseNgTagNameSet.has(this.name.toUpperCase())
  732. if (pre !== this.ng) this.emit('ngChanged', this.ng)
  733. },
  734. updateNgIfLocked(upperCaseNgTagNameSet) {
  735. if (!this.lock) return;
  736. const pre = this.ng;
  737. this.ngByLock = upperCaseNgTagNameSet.has(this.name.toUpperCase());
  738. if (pre !== this.ng) this.emit('ngChanged', this.ng);
  739. },
  740. })
  741. return Tag
  742. })(EventEmitter)
  743.  
  744. var Contributor = (function(_super) {
  745. var Contributor = function(type, id, name) {
  746. _super.call(this)
  747. this.type = type
  748. this.id = id
  749. this.name = name
  750. this.ng = false
  751. this.ngId = false
  752. this.ngName = ''
  753. }
  754. Contributor.prototype = createObject(_super.prototype, {
  755. _updateNg() {
  756. var pre = this.ng
  757. this.ng = this.ngId || Boolean(this.ngName)
  758. if (pre !== this.ng) this.emit('ngChanged', this.ng)
  759. },
  760. updateNgId(ngIdSet) {
  761. var pre = this.ngId
  762. this.ngId = ngIdSet.has(this.id)
  763. if (pre !== this.ngId) this.emit('ngIdChanged', this.ngId)
  764. this._updateNg()
  765. },
  766. _getNewNgName(upperCaseNgNameSet) {
  767. var n = this.name.toUpperCase()
  768. for (var ngName of upperCaseNgNameSet)
  769. if (n.includes(ngName)) return ngName
  770. return ''
  771. },
  772. updateNgName(upperCaseNgNameSet) {
  773. var pre = this.ngName
  774. this.ngName = this._getNewNgName(upperCaseNgNameSet)
  775. if (pre !== this.ngName) this.emit('ngNameChanged', this.ngName)
  776. this._updateNg()
  777. },
  778. get url() {
  779. throw new Error('must be implemented')
  780. },
  781. bindToConfig(config) {
  782. this.updateNgId(config[this.ngIdStoreName].set)
  783. config[this.ngIdStoreName].on('changed', this.updateNgId.bind(this))
  784. },
  785. })
  786.  
  787. var User = function(id, name) {
  788. Contributor.call(this, 'user', id, name)
  789. }
  790. User.prototype = createObject(Contributor.prototype, {
  791. get ngIdStoreName() { return 'ngUserIds' },
  792. get url() {
  793. return 'https://www.nicovideo.jp/user/' + this.id
  794. },
  795. bindToConfig(config) {
  796. Contributor.prototype.bindToConfig.call(this, config)
  797. this.updateNgName(config.ngUserNames.set)
  798. config.ngUserNames.on('changed', this.updateNgName.bind(this))
  799. },
  800. })
  801.  
  802. var Channel = function(id, name) {
  803. Contributor.call(this, 'channel', id, name)
  804. }
  805. Channel.prototype = createObject(Contributor.prototype, {
  806. get ngIdStoreName() { return 'ngChannelIds' },
  807. get url() {
  808. return 'https://ch.nicovideo.jp/channel/ch' + this.id
  809. },
  810. })
  811.  
  812. Object.assign(Contributor, {
  813. NULL: new Contributor('unknown', -1, ''),
  814. TYPES: ['user', 'channel'],
  815. new(type, id, name) {
  816. switch (type) {
  817. case 'user': return new User(id, name)
  818. case 'channel': return new Channel(id, name)
  819. default: throw new Error(type)
  820. }
  821. },
  822. })
  823. return Contributor
  824. })(EventEmitter)
  825.  
  826. var Movie = (function(_super) {
  827. var Movie = function(id, title) {
  828. _super.call(this)
  829. this.id = id
  830. this.title = title
  831. this.ngTitle = ''
  832. this.ngId = false
  833. this.visited = false
  834. this._tags = []
  835. this._contributor = Contributor.NULL
  836. this._description = ''
  837. this._error = Movie.NO_ERROR
  838. this._thumbInfoDone = false
  839. this._ng = false
  840. }
  841. Movie.NO_ERROR = {type: 'NO_ERROR', message: 'no error'}
  842. Movie.prototype = createObject(_super.prototype, {
  843. _matchedNgTitle(upperCaseNgTitleSet) {
  844. var t = this.title.toUpperCase()
  845. for (var ng of upperCaseNgTitleSet) {
  846. if (t.includes(ng)) return ng
  847. }
  848. return ''
  849. },
  850. updateNgTitle(upperCaseNgTitleSet) {
  851. var pre = this.ngTitle
  852. this.ngTitle = this._matchedNgTitle(upperCaseNgTitleSet)
  853. if (pre === this.ngTitle) return
  854. this.emit('ngTitleChanged', this.ngTitle)
  855. this._updateNg()
  856. },
  857. updateNgId(ngIdSet) {
  858. var pre = this.ngId
  859. this.ngId = ngIdSet.has(this.id)
  860. if (pre === this.ngId) return
  861. this.emit('ngIdChanged', this.ngId)
  862. this._updateNg()
  863. },
  864. updateVisited(visitedIdSet) {
  865. var pre = this.visited
  866. this.visited = visitedIdSet.has(this.id)
  867. if (pre !== this.visited) this.emit('visitedChanged', this.visited)
  868. },
  869. get description() { return this._description },
  870. set description(description) {
  871. this._description = description
  872. this.emit('descriptionChanged', this._description)
  873. },
  874. get tags() { return this._tags },
  875. set tags(tags) {
  876. this._tags = tags
  877. this.emit('tagsChanged', this._tags)
  878. this._updateNg()
  879. var update = this._updateNg.bind(this)
  880. for (var t of this._tags) t.on('ngChanged', update)
  881. },
  882. get contributor() { return this._contributor },
  883. set contributor(contributor) {
  884. this._contributor = contributor
  885. this.emit('contributorChanged', this._contributor)
  886. this._updateNg()
  887. this._contributor.on('ngChanged', this._updateNg.bind(this))
  888. },
  889. get error() { return this._error },
  890. set error(error) {
  891. this._error = error
  892. this.emit('errorChanged', this._error)
  893. },
  894. get thumbInfoDone() { return this._thumbInfoDone },
  895. setThumbInfoDone() {
  896. this._thumbInfoDone = true
  897. this.emit('thumbInfoDone')
  898. },
  899. get ng() { return this._ng },
  900. _updateNg() {
  901. var pre = this._ng
  902. this._ng = this.ngId
  903. || Boolean(this.ngTitle)
  904. || this.contributor.ng
  905. || this.tags.some(function(t) { return t.ng })
  906. if (pre !== this._ng) this.emit('ngChanged', this._ng)
  907. },
  908. addListenerToConfig(config) {
  909. config.ngMovies.on('changed', this.updateNgId.bind(this))
  910. config.ngTitles.on('changed', this.updateNgTitle.bind(this))
  911. config.visitedMovies.on('changed', this.updateVisited.bind(this))
  912. },
  913. })
  914. return Movie
  915. })(EventEmitter)
  916.  
  917. var Movies = (function() {
  918. var Movies = function(config) {
  919. this.config = config
  920. this._idToMovie = new Map()
  921. }
  922. Movies.prototype = {
  923. setIfAbsent(movies) {
  924. var ngIds = this.config.ngMovies.set
  925. var ngTitles = this.config.ngTitles.set
  926. var visitedIds = this.config.visitedMovies.set
  927. var map = this._idToMovie
  928. for (var m of movies) {
  929. if (map.has(m.id)) continue
  930. map.set(m.id, m)
  931. m.updateNgId(ngIds)
  932. m.updateNgTitle(ngTitles)
  933. m.updateVisited(visitedIds)
  934. m.addListenerToConfig(this.config)
  935. }
  936. },
  937. get(movieId) {
  938. return this._idToMovie.get(movieId)
  939. },
  940. }
  941. return Movies
  942. })()
  943.  
  944. var ThumbInfoListener = (function() {
  945. var createTagBuilder = function(config) {
  946. var map = new Map()
  947. return thumbInfoTag => {
  948. let a;
  949. const i = thumbInfoTag.lock ? 1 : 0;
  950. if (map.has(thumbInfoTag.name)) {
  951. a = map.get(thumbInfoTag.name);
  952. if (a[i]) return a[i];
  953. } else {
  954. a = [null, null];
  955. }
  956. const tag = new Tag(thumbInfoTag);
  957. a[i] = tag;
  958. map.set(thumbInfoTag.name, a);
  959. config.ngTags.on('changed', tagNameSet => tag.updateNg(tagNameSet));
  960. config.ngLockedTags.on('changed', tagNameSet => tag.updateNgIfLocked(tagNameSet));
  961. return tag;
  962. };
  963. }
  964. var createTagsBuilder = function(config) {
  965. var getTagBy = createTagBuilder(config)
  966. return thumbInfoTags => {
  967. const tags = thumbInfoTags.map(getTagBy);
  968. const ngTagSet = config.ngTags.set;
  969. const ngLockedTagSet = config.ngLockedTags.set;
  970. for (const t of tags) {
  971. t.updateNg(ngTagSet);
  972. t.updateNgIfLocked(ngLockedTagSet);
  973. }
  974. return tags;
  975. };
  976. }
  977. var createContributorBuilder = function(config) {
  978. var typeToMap = Contributor.TYPES.reduce(function(map, type) {
  979. return map.set(type, new Map())
  980. }, new Map())
  981. return function(o) {
  982. var map = typeToMap.get(o.type)
  983. if (map.has(o.id)) return map.get(o.id)
  984. var contributor = Contributor.new(o.type, o.id, o.name)
  985. map.set(o.id, contributor)
  986. contributor.bindToConfig(config)
  987. return contributor
  988. }
  989. }
  990. return {
  991. forCompleted(movies) {
  992. var getTagsBy = createTagsBuilder(movies.config)
  993. var getContributorBy = createContributorBuilder(movies.config)
  994. return function(thumbInfo) {
  995. var m = movies.get(thumbInfo.id)
  996. m.description = thumbInfo.description
  997. m.tags = getTagsBy(thumbInfo.tags)
  998. m.contributor = getContributorBy(thumbInfo.contributor)
  999. m.setThumbInfoDone()
  1000. }
  1001. },
  1002. forErrorOccurred(movies) {
  1003. return function(thumbInfo) {
  1004. var m = movies.get(thumbInfo.id)
  1005. m.error = thumbInfo.error
  1006. m.setThumbInfoDone()
  1007. }
  1008. },
  1009. }
  1010. })()
  1011.  
  1012. var MovieViewMode = (function(_super) {
  1013. var MovieViewMode = function(movie, config) {
  1014. _super.call(this)
  1015. this.movie = movie
  1016. this.config = config
  1017. this.value = this._newViewMode()
  1018. }
  1019. MovieViewMode.prototype = createObject(_super.prototype, {
  1020. _isHiddenByNg() {
  1021. return !this.config.ngMovieVisible.value && this.movie.ng
  1022. },
  1023. _isHiddenByContributorType() {
  1024. var c = this.movie.contributor
  1025. if (c === Contributor.NULL) return false
  1026. var t = this.config.visibleContributorType.value
  1027. return !(t === 'all' || t === c.type)
  1028. },
  1029. _isHiddenByVisitedMovieViewMode() {
  1030. return this.movie.visited
  1031. && this.config.visitedMovieViewMode.value === 'hide'
  1032. },
  1033. _isHidden() {
  1034. return this.movie.error.type === 'DELETED'
  1035. || this._isHiddenByContributorType()
  1036. || this._isHiddenByNg()
  1037. || this._isHiddenByVisitedMovieViewMode()
  1038. },
  1039. _isReduced() {
  1040. return this.movie.visited
  1041. && this.config.visitedMovieViewMode.value === 'reduce'
  1042. },
  1043. _newViewMode() {
  1044. if (this._isHidden()) return 'hide'
  1045. if (this._isReduced()) return 'reduce'
  1046. return 'doNothing'
  1047. },
  1048. update() {
  1049. var pre = this.value
  1050. this.value = this._newViewMode()
  1051. if (pre !== this.value) this.emit('changed', this.value)
  1052. },
  1053. addListener() {
  1054. var l = this.update.bind(this)
  1055. this.movie
  1056. .on('errorChanged', l)
  1057. .on('ngChanged', l)
  1058. .on('visitedChanged', l)
  1059. .on('contributorChanged', l)
  1060. ;['ngMovieVisible',
  1061. 'visibleContributorType',
  1062. 'visitedMovieViewMode',
  1063. ].forEach(function(n) {
  1064. this.config[n].on('changed', l)
  1065. }, this)
  1066. return this
  1067. },
  1068. })
  1069. return MovieViewMode
  1070. })(EventEmitter)
  1071.  
  1072. var MovieViewModes = (function(_super) {
  1073. var MovieViewModes = function(config) {
  1074. _super.call(this)
  1075. this.config = config
  1076. this._movieToViewMode = new Map()
  1077. this._emitViewModeChanged = this.emit.bind(this, 'movieViewModeChanged')
  1078. }
  1079. MovieViewModes.prototype = createObject(_super.prototype, {
  1080. get(movie) {
  1081. var m = this._movieToViewMode
  1082. if (m.has(movie)) return m.get(movie)
  1083. var viewMode = new MovieViewMode(movie, this.config)
  1084. m.set(movie, viewMode)
  1085. return viewMode.on('changed', this._emitViewModeChanged).addListener()
  1086. },
  1087. sort() {
  1088. return [...this._movieToViewMode.values()].map(function(m, i) {
  1089. return {i, m}
  1090. }).sort(function(a, b) {
  1091. if (a.m.value === 'hide' && b.m.value !== 'hide') return 1
  1092. if (a.m.value !== 'hide' && b.m.value === 'hide') return -1
  1093. return a.i - b.i
  1094. }).map(function(o) {
  1095. return o.m
  1096. })
  1097. },
  1098. })
  1099. return MovieViewModes
  1100. })(EventEmitter)
  1101.  
  1102. var ConfigDialog = (function(_super) {
  1103. var isValidStr = function(s) {
  1104. return typeof s === 'string' && Boolean(s.trim().length)
  1105. }
  1106. var isPositiveInt = function(n) {
  1107. return Number.isSafeInteger(n) && n > 0
  1108. }
  1109. var initCheckbox = function(config, doc, name) {
  1110. var b = doc.getElementById(name)
  1111. b.checked = config[name].value
  1112. b.addEventListener('change', function() {
  1113. config[name].value = b.checked
  1114. })
  1115. }
  1116. var optionOf = function(v) {
  1117. return typeof v === 'object'
  1118. ? new Option(v.value + ',' + v.text, v.value)
  1119. : new Option(v, v)
  1120. }
  1121. var diffBy = function(target) {
  1122. var SOMETHING_INPUT_TEXT = '何か入力して下さい。'
  1123. var POSITIVE_INT_INPUT_TEXT = '1以上の整数を入力して下さい。'
  1124. var movieUrlOf = function(movieId) {
  1125. return 'https://www.nicovideo.jp/watch/' + movieId
  1126. }
  1127. return {
  1128. 'ng-movie-id': {
  1129. targetText: 'NG動画ID',
  1130. storeName: 'ngMovies',
  1131. convert(v) { return v },
  1132. isValid: isValidStr,
  1133. inputRequestText: SOMETHING_INPUT_TEXT,
  1134. urlOf: movieUrlOf,
  1135. },
  1136. 'ng-title': {
  1137. targetText: 'NGタイトル',
  1138. storeName: 'ngTitles',
  1139. convert(v) { return v },
  1140. isValid: isValidStr,
  1141. inputRequestText: SOMETHING_INPUT_TEXT,
  1142. urlOf(title) { return 'https://www.nicovideo.jp/search/' + title },
  1143. },
  1144. 'ng-tag': {
  1145. targetText: 'NGタグ',
  1146. storeName: 'ngTags',
  1147. convert(v) { return v },
  1148. isValid: isValidStr,
  1149. inputRequestText: SOMETHING_INPUT_TEXT,
  1150. urlOf(tag) { return 'https://www.nicovideo.jp/tag/' + tag },
  1151. },
  1152. 'ng-locked-tag': {
  1153. targetText: 'NGタグ(ロック)',
  1154. storeName: 'ngLockedTags',
  1155. convert(v) { return v },
  1156. isValid: isValidStr,
  1157. inputRequestText: SOMETHING_INPUT_TEXT,
  1158. urlOf(tag) { return 'https://www.nicovideo.jp/tag/' + tag },
  1159. },
  1160. 'ng-user-id': {
  1161. targetText: 'NGユーザーID',
  1162. storeName: 'ngUserIds',
  1163. convert: Math.trunc,
  1164. isValid(v) { return isPositiveInt(Math.trunc(v)) },
  1165. inputRequestText: POSITIVE_INT_INPUT_TEXT,
  1166. urlOf(userId) { return 'https://www.nicovideo.jp/user/' + userId },
  1167. },
  1168. 'ng-user-name': {
  1169. targetText: 'NGユーザー名',
  1170. storeName: 'ngUserNames',
  1171. convert(v) { return v },
  1172. isValid: isValidStr,
  1173. inputRequestText: SOMETHING_INPUT_TEXT,
  1174. urlOf(userName) { return 'https://www.nicovideo.jp/search/' + userName },
  1175. },
  1176. 'ng-channel-id': {
  1177. targetText: 'NGチャンネルID',
  1178. storeName: 'ngChannelIds',
  1179. convert: Math.trunc,
  1180. isValid(v) { return isPositiveInt(Math.trunc(v)) },
  1181. inputRequestText: POSITIVE_INT_INPUT_TEXT,
  1182. urlOf(channelId) { return 'https://ch.nicovideo.jp/ch' + channelId },
  1183. },
  1184. 'visited-movie-id': {
  1185. targetText: '閲覧済み動画ID',
  1186. storeName: 'visitedMovies',
  1187. convert(v) { return v },
  1188. isValid: isValidStr,
  1189. inputRequestText: SOMETHING_INPUT_TEXT,
  1190. urlOf: movieUrlOf,
  1191. },
  1192. }[target]
  1193. }
  1194. var promptFor = async function(target, config, defaultValue) {
  1195. var d = diffBy(target)
  1196. var r = ''
  1197. do {
  1198. var msg = r ? `"${r}"は登録済みです。\n` : ''
  1199. r = window.prompt(msg + d.targetText, r || defaultValue || '')
  1200. if (r === null) return ''
  1201. while (!d.isValid(r)) {
  1202. r = window.prompt(d.inputRequestText + '\n' + d.targetText)
  1203. if (r === null) return ''
  1204. }
  1205. } while (!(await config[d.storeName].addAsync(d.convert(r))))
  1206. return r
  1207. }
  1208.  
  1209. var ConfigDialog = function(config, doc, openInTab) {
  1210. _super.call(this)
  1211. this.config = config
  1212. this.doc = doc
  1213. this.openInTab = openInTab
  1214. for (var v of config.ngTitles.array) {
  1215. this._e('list').add(new Option(v, v))
  1216. }
  1217. this._e('removeAllButton').disabled = !config.ngTitles.array.length
  1218. initCheckbox(config, doc, 'openNewWindow')
  1219. initCheckbox(config, doc, 'useGetThumbInfo')
  1220. initCheckbox(config, doc, 'movieInfoTogglable')
  1221. initCheckbox(config, doc, 'descriptionTogglable')
  1222. initCheckbox(config, doc, 'addToNgLockedTags')
  1223. this._on('target', 'change', this._targetChanged.bind(this))
  1224. this._on('addButton', 'click', this._addButtonClicked.bind(this))
  1225. this._on('removeButton', 'click', this._removeButtonClicked.bind(this))
  1226. this._on('removeAllButton', 'click', this._removeAllButtonClicked.bind(this))
  1227. this._on('openButton', 'click', this._openButtonClicked.bind(this))
  1228. this._on('closeButton', 'click', this.emit.bind(this, 'closed'))
  1229. this._on('exportVisibleCheckbox', 'change', this._exportVisibleCheckboxChanged.bind(this))
  1230. this._on('importVisibleCheckbox', 'change', this._importVisibleCheckboxChanged.bind(this))
  1231. this._on('exportButton', 'click', this._exportButtonClicked.bind(this))
  1232. this._on('importButton', 'click', this._importButtonClicked.bind(this))
  1233. var updateButtonsDisabled = this._updateButtonsDisabled.bind(this)
  1234. this._on('target', 'change', updateButtonsDisabled)
  1235. this._on('list', 'change', updateButtonsDisabled)
  1236. this._on('addButton', 'click', updateButtonsDisabled)
  1237. this._on('removeButton', 'click', updateButtonsDisabled)
  1238. this._on('removeAllButton', 'click', updateButtonsDisabled)
  1239. }
  1240. ConfigDialog.prototype = createObject(_super.prototype, {
  1241. _e(id) { return this.doc.getElementById(id) },
  1242. _on(id, eventName, listener) {
  1243. this._e(id).addEventListener(eventName, listener)
  1244. },
  1245. _diffBySelectedTarget() {
  1246. return diffBy(this._e('target').value)
  1247. },
  1248. _updateList() {
  1249. for (var o of Array.from(this._e('list').options)) o.remove()
  1250. var d = this._diffBySelectedTarget()
  1251. for (var val of this.config[d.storeName].arrayWithText) {
  1252. this._e('list').add(optionOf(val))
  1253. }
  1254. },
  1255. _targetChanged() {
  1256. this._updateList()
  1257. },
  1258. _updateButtonsDisabled() {
  1259. var l = this._e('list')
  1260. var d = l.selectedIndex === -1
  1261. this._e('removeButton').disabled = d
  1262. this._e('openButton').disabled = d
  1263. this._e('removeAllButton').disabled = !l.length
  1264. },
  1265. async _addButtonClicked() {
  1266. var r = await promptFor(this._e('target').value, this.config)
  1267. if (r) this._e('list').add(new Option(r, r))
  1268. },
  1269. async _removeButtonClicked() {
  1270. var opts = Array.from(this._e('list').selectedOptions)
  1271. var d = this._diffBySelectedTarget()
  1272. await this.config[d.storeName]
  1273. .removeAsync(opts.map(function(o) { return d.convert(o.value) }))
  1274. for (var o of opts) o.remove()
  1275. },
  1276. _removeAllButtonClicked() {
  1277. var d = this._diffBySelectedTarget()
  1278. if (!window.confirm(`すべての"${d.targetText}"を削除しますか?`)) return
  1279. this.config[d.storeName].clear()
  1280. for (var o of Array.from(this._e('list').options)) o.remove()
  1281. },
  1282. _openButtonClicked() {
  1283. var opts = Array.from(this._e('list').selectedOptions)
  1284. var d = this._diffBySelectedTarget()
  1285. for (var v of opts.map(function(o) { return o.value })) {
  1286. this.openInTab(d.urlOf(v))
  1287. }
  1288. },
  1289. _exportVisibleCheckboxChanged() {
  1290. var n = this._e('exportVisibleCheckbox').checked ? 'remove' : 'add'
  1291. this._e('exportContainer').classList[n]('isHidden')
  1292. },
  1293. _importVisibleCheckboxChanged() {
  1294. var n = this._e('importVisibleCheckbox').checked ? 'remove' : 'add'
  1295. this._e('importContainer').classList[n]('isHidden')
  1296. },
  1297. async _exportButtonClicked() {
  1298. var textarea = this._e('exportTextarea')
  1299. textarea.value = await this.config.toCSV({
  1300. ngMovieId: this._e('exportNgMovieIdCheckbox').checked,
  1301. ngTitle: this._e('exportNgTitleCheckbox').checked,
  1302. ngTag: this._e('exportNgTagCheckbox').checked,
  1303. ngUserId: this._e('exportNgUserIdCheckbox').checked,
  1304. ngUserName: this._e('exportNgUserNameCheckbox').checked,
  1305. ngChannelId: this._e('exportNgChannelIdCheckbox').checked,
  1306. visitedMovieId: this._e('exportVisitedMovieIdCheckbox').checked,
  1307. })
  1308. textarea.focus()
  1309. textarea.select()
  1310. },
  1311. async _importButtonClicked() {
  1312. await this.config.addFromCSV(this._e('importTextarea').value)
  1313. this._updateList()
  1314. this._e('importTextarea').value = ''
  1315. },
  1316. })
  1317. ConfigDialog.promptNgTitle = function(config, defaultValue) {
  1318. promptFor('ng-title', config, defaultValue)
  1319. }
  1320. ConfigDialog.promptNgUserName = function(config, defaultValue) {
  1321. promptFor('ng-user-name', config, defaultValue)
  1322. }
  1323. ConfigDialog.SRCDOC = `<!doctype html>
  1324. <html><head><style>
  1325. html {
  1326. margin: 0 auto;
  1327. max-width: 30em;
  1328. height: 100%;
  1329. line-height: 1.5em;
  1330. }
  1331. body {
  1332. height: 100%;
  1333. margin: 0;
  1334. display: flex;
  1335. flex-direction: column;
  1336. justify-content: center;
  1337. }
  1338. .dialog {
  1339. overflow: auto;
  1340. padding: 8px;
  1341. background-color: white;
  1342. }
  1343. p {
  1344. margin: 0;
  1345. }
  1346. .listButtonsWrap {
  1347. display: flex;
  1348. }
  1349. .listButtonsWrap .list {
  1350. flex: auto;
  1351. }
  1352. .listButtonsWrap .list select {
  1353. width: 100%;
  1354. }
  1355. .listButtonsWrap .buttons {
  1356. flex: none;
  1357. display: flex;
  1358. flex-direction: column;
  1359. }
  1360. .listButtonsWrap .buttons input {
  1361. margin-bottom: 5px;
  1362. }
  1363. .sideComment {
  1364. margin-left: 2em;
  1365. }
  1366. .dialogBottom {
  1367. text-align: center;
  1368. }
  1369. .scriptInfo {
  1370. text-align: right;
  1371. }
  1372. .isHidden {
  1373. display: none;
  1374. }
  1375. textarea {
  1376. width: 100%;
  1377. }
  1378. p:has(#addToNgLockedTags) {
  1379. display: flex;
  1380. }
  1381. </style></head><body>
  1382. <div class=dialog>
  1383. <p><select id=target>
  1384. <option value=ng-movie-id>NG動画ID</option>
  1385. <option value=ng-title selected>NGタイトル</option>
  1386. <option value=ng-tag>NGタグ</option>
  1387. <option value=ng-locked-tag>NGタグ(ロック)</option>
  1388. <option value=ng-user-id>NGユーザーID</option>
  1389. <option value=ng-user-name>NGユーザー名</option>
  1390. <option value=ng-channel-id>NGチャンネルID</option>
  1391. <option value=visited-movie-id>閲覧済み動画ID</option>
  1392. </select></p>
  1393. <div class=listButtonsWrap>
  1394. <p class=list><select multiple size=10 id=list></select></p>
  1395. <p class=buttons>
  1396. <input type=button value=追加 id=addButton>
  1397. <input type=button value=削除 disabled id=removeButton>
  1398. <input type=button value=全削除 disabled id=removeAllButton>
  1399. <input type=button value=開く disabled id=openButton>
  1400. </p>
  1401. </div>
  1402. <p><input type=checkbox id=addToNgLockedTags><label for=addToNgLockedTags>ロックされたタグを[+]ボタンでNG登録するとき、「NGタグ(ロック)」に追加する</label></p>
  1403. <p><label><input type=checkbox id=openNewWindow>動画を別窓で開く</label></p>
  1404. <p><label><input type=checkbox id=useGetThumbInfo>動画情報を取得する</label></p>
  1405. <fieldset id=togglable>
  1406. <legend>表示・非表示の切り替えボタン</legend>
  1407. <p><label><input type=checkbox id=movieInfoTogglable>タグ、ユーザー、チャンネル</label></p>
  1408. <p><label><input type=checkbox id=descriptionTogglable>動画説明</label></p>
  1409. </fieldset>
  1410. <p>エクスポート<small><label><input id=exportVisibleCheckbox type=checkbox>表示</label></small></p>
  1411. <div id=exportContainer class=isHidden>
  1412. <p><label><input id=exportNgMovieIdCheckbox type=checkbox checked>NG動画ID</label></p>
  1413. <p><label><input id=exportNgTitleCheckbox type=checkbox checked>NGタイトル</label></p>
  1414. <p><label><input id=exportNgTagCheckbox type=checkbox checked>NGタグ</label></p>
  1415. <p><label><input id=exportNgUserIdCheckbox type=checkbox checked>NGユーザーID</label></p>
  1416. <p><label><input id=exportNgUserNameCheckbox type=checkbox checked>NGユーザー名</label></p>
  1417. <p><label><input id=exportNgChannelIdCheckbox type=checkbox checked>NGチャンネルID</label></p>
  1418. <p><label><input id=exportVisitedMovieIdCheckbox type=checkbox checked>閲覧済み動画ID</label></p>
  1419. <p><input id=exportButton type=button value=エクスポート></p>
  1420. <p><textarea id=exportTextarea rows=3></textarea></p>
  1421. </div>
  1422. <p>インポート<small><label><input id=importVisibleCheckbox type=checkbox>表示</label></small></p>
  1423. <div id=importContainer class=isHidden>
  1424. <p><textarea id=importTextarea rows=3></textarea></p>
  1425. <p><input id=importButton type=button value=インポート></p>
  1426. </div>
  1427. <p class=dialogBottom><input type=button value=閉じる id=closeButton></p>
  1428. <p class=scriptInfo><small><a href=https://greasyfork.org/ja/scripts/880-nico-nico-ranking-ng target=_blank>Nico Nico Ranking NG</a></small></p>
  1429. </div>
  1430. </body></html>`
  1431. return ConfigDialog
  1432. })(EventEmitter)
  1433.  
  1434. var NicoPage = (function() {
  1435. var TOGGLE_OPEN_TEXT = '▼'
  1436. var TOGGLE_CLOSE_TEXT = '▲'
  1437. var emphasizeMatchedText = function(e, text, createMatchedElem) {
  1438. var t = e.textContent
  1439. if (!text) {
  1440. e.textContent = t
  1441. return
  1442. }
  1443. var i = t.toUpperCase().indexOf(text)
  1444. if (i === -1) {
  1445. e.textContent = t
  1446. return
  1447. }
  1448. while (e.hasChildNodes()) e.removeChild(e.firstChild)
  1449. var d = e.ownerDocument
  1450. if (i !== 0) e.appendChild(d.createTextNode(t.slice(0, i)))
  1451. e.appendChild(createMatchedElem(t.slice(i, i + text.length)))
  1452. if (i + text.length !== t.length) {
  1453. e.appendChild(d.createTextNode(t.slice(i + text.length)))
  1454. }
  1455. }
  1456.  
  1457. var MovieTitle = (function() {
  1458. var MovieTitle = function(elem) {
  1459. this.elem = elem
  1460. this._ngTitle = ''
  1461. this._listeners = new Listeners({
  1462. ngIdChanged: set(this, 'ngId'),
  1463. ngTitleChanged: set(this, 'ngTitle'),
  1464. })
  1465. }
  1466. MovieTitle.prototype = {
  1467. get ngId() {
  1468. return this.elem.classList.contains('nrn-ng-movie-title')
  1469. },
  1470. set ngId(ngId) {
  1471. var n = ngId ? 'add' : 'remove'
  1472. this.elem.classList[n]('nrn-ng-movie-title')
  1473. },
  1474. _createNgTitleElem(textContent) {
  1475. var result = this.elem.ownerDocument.createElement('span')
  1476. result.className = 'nrn-matched-ng-title'
  1477. result.textContent = textContent
  1478. return result
  1479. },
  1480. get ngTitle() { return this._ngTitle },
  1481. set ngTitle(ngTitle) {
  1482. this._ngTitle = ngTitle
  1483. emphasizeMatchedText(this.elem, ngTitle, this._createNgTitleElem.bind(this))
  1484. },
  1485. bindToMovie(movie) {
  1486. this.ngId = movie.ngId
  1487. this.ngTitle = movie.ngTitle
  1488. this._listeners.bind(movie)
  1489. return this
  1490. },
  1491. unbind() {
  1492. this._listeners.unbind()
  1493. },
  1494. }
  1495. return MovieTitle
  1496. })()
  1497.  
  1498. var ActionPane = (function() {
  1499. var createVisitButton = function(doc, movie) {
  1500. var result = doc.createElement('span')
  1501. result.className = 'nrn-visit-button'
  1502. result.textContent = '閲覧済み'
  1503. result.dataset.movieId = movie.id
  1504. result.dataset.type = 'add'
  1505. result.dataset.movieTitle = movie.title
  1506. return result
  1507. }
  1508. var createMovieNgButton = function(doc, movie) {
  1509. var result = doc.createElement('span')
  1510. result.className = 'nrn-movie-ng-button'
  1511. result.textContent = 'NG動画'
  1512. result.dataset.movieId = movie.id
  1513. result.dataset.type = 'add'
  1514. result.dataset.movieTitle = movie.title
  1515. return result
  1516. }
  1517. var createTitleNgButton = function(doc, movie) {
  1518. var result = doc.createElement('span')
  1519. result.className = 'nrn-title-ng-button'
  1520. result.textContent = 'NGタイトル追加'
  1521. result.dataset.movieTitle = movie.title
  1522. result.dataset.ngTitle = ''
  1523. return result
  1524. }
  1525. var createPane = function(doc) {
  1526. var result = doc.createElement('div')
  1527. result.className = 'nrn-action-pane'
  1528. for (var c of Array.from(arguments).slice(1)) result.appendChild(c)
  1529. return result
  1530. }
  1531. var ActionPane = function(doc, movie) {
  1532. this.elem = createPane(doc
  1533. , createVisitButton(doc, movie)
  1534. , createMovieNgButton(doc, movie)
  1535. , createTitleNgButton(doc, movie))
  1536. this._listeners = new Listeners({
  1537. ngIdChanged: set(this, 'ngId'),
  1538. ngTitleChanged: set(this, 'ngTitle'),
  1539. visitedChanged: set(this, 'visited'),
  1540. })
  1541. }
  1542. ActionPane.prototype = {
  1543. get _visitButton() {
  1544. return this.elem.querySelector('.nrn-visit-button')
  1545. },
  1546. get visited() {
  1547. return this._visitButton.dataset.type === 'remove'
  1548. },
  1549. set visited(visited) {
  1550. var b = this._visitButton
  1551. b.textContent = visited ? '未閲覧' : '閲覧済み'
  1552. b.dataset.type = visited ? 'remove' : 'add'
  1553. },
  1554. get _movieNgButton() {
  1555. return this.elem.querySelector('.nrn-movie-ng-button')
  1556. },
  1557. get ngId() {
  1558. return this._movieNgButton.dataset.type === 'remove'
  1559. },
  1560. set ngId(ngId) {
  1561. var b = this._movieNgButton
  1562. b.textContent = ngId ? 'NG解除' : 'NG登録'
  1563. b.dataset.type = ngId ? 'remove' : 'add'
  1564. },
  1565. get _titleNgButton() {
  1566. return this.elem.querySelector('.nrn-title-ng-button')
  1567. },
  1568. get ngTitle() {
  1569. return this._titleNgButton.dataset.ngTitle
  1570. },
  1571. set ngTitle(ngTitle) {
  1572. var b = this._titleNgButton
  1573. b.textContent = ngTitle ? 'NGタイトル削除' : 'NGタイトル追加'
  1574. b.dataset.type = ngTitle ? 'remove' : 'add'
  1575. b.dataset.ngTitle = ngTitle
  1576. },
  1577. bindToMovie(movie) {
  1578. this.ngId = movie.ngId
  1579. this.ngTitle = movie.ngTitle
  1580. this.visited = movie.visited
  1581. this._listeners.bind(movie)
  1582. return this
  1583. },
  1584. unbind() {
  1585. this._listeners.unbind()
  1586. },
  1587. }
  1588. return ActionPane
  1589. })()
  1590.  
  1591. var TagView = (function() {
  1592. var createElem = function(doc, tag) {
  1593. var a = doc.createElement('a')
  1594. a.className = 'nrn-movie-tag-link'
  1595. a.target = '_blank'
  1596. a.textContent = tag.name
  1597. a.href = 'https://www.nicovideo.jp/tag/' + tag.name
  1598. const key = doc.createElement('span');
  1599. key.textContent = tag.lock ? '🔒' : '';
  1600. var b = doc.createElement('span')
  1601. b.className = 'nrn-tag-ng-button'
  1602. b.textContent = '[+]'
  1603. b.dataset.type = 'add'
  1604. b.dataset.tagName = tag.name
  1605. if (tag.lock) b.dataset.lock = 'true';
  1606. var result = doc.createElement('span')
  1607. result.className = 'nrn-movie-tag'
  1608. result.appendChild(a)
  1609. result.appendChild(key);
  1610. result.appendChild(b)
  1611. return result
  1612. }
  1613. var TagView = function(doc, tag) {
  1614. this.tagName = tag.name;
  1615. this.elem = createElem(doc, tag);
  1616. this._listeners = new Listeners({ngChanged: set(this, 'ng')})
  1617. }
  1618. TagView.prototype = {
  1619. get _link() {
  1620. return this.elem.querySelector('.nrn-movie-tag-link')
  1621. },
  1622. get ng() {
  1623. return this._link.classList.contains('nrn-movie-ng-tag-link')
  1624. },
  1625. set ng(ng) {
  1626. this._link.classList[ng ? 'add' : 'remove']('nrn-movie-ng-tag-link')
  1627. var b = this.elem.querySelector('.nrn-tag-ng-button')
  1628. b.textContent = ng ? '[x]' : '[+]'
  1629. b.dataset.type = ng ? 'remove' : 'add'
  1630. },
  1631. bindToTag(tag) {
  1632. this.ng = tag.ng
  1633. this._listeners.bind(tag)
  1634. return this
  1635. },
  1636. unbind() {
  1637. this._listeners.unbind()
  1638. },
  1639. }
  1640. return TagView
  1641. })()
  1642.  
  1643. var ContributorView = (function() {
  1644. var ContributorView = function(doc, contributor) {
  1645. this.contributor = contributor
  1646. this.elem = this._createElem(doc)
  1647. }
  1648. ContributorView.prototype = {
  1649. _createElem(doc) {
  1650. var a = doc.createElement('a')
  1651. a.className = 'nrn-contributor-link'
  1652. a.target = '_blank'
  1653. a.href = this.contributor.url
  1654. a.textContent = this.contributor.name || '(名前不明)'
  1655. var b = doc.createElement('span')
  1656. this._setNgButton(b)
  1657. var result = doc.createElement('span')
  1658. result.className = 'nrn-contributor'
  1659. result.appendChild(doc.createTextNode(this._label))
  1660. result.appendChild(a)
  1661. result.appendChild(b)
  1662. return result
  1663. },
  1664. _initContributorDataset(dataset) {
  1665. dataset.contributorType = this.contributor.type
  1666. dataset.id = this.contributor.id
  1667. dataset.name = this.contributor.name
  1668. dataset.type = 'add'
  1669. },
  1670. get _label() {
  1671. throw new Error('must be implemented')
  1672. },
  1673. _setNgButton() {
  1674. throw new Error('must be implemented')
  1675. },
  1676. _bindToContributor() {
  1677. throw new Error('must be implemented')
  1678. },
  1679. }
  1680.  
  1681. var UserView = function UserView(doc, contributor) {
  1682. ContributorView.call(this, doc, contributor)
  1683. this._listeners = new Listeners({
  1684. ngIdChanged: set(this, 'ngId'),
  1685. ngNameChanged: set(this, 'ngName'),
  1686. })
  1687. this._bindToContributor()
  1688. }
  1689. UserView.prototype = createObject(ContributorView.prototype, {
  1690. get _label() {
  1691. return 'ユーザー: '
  1692. },
  1693. _setNgButton(b) {
  1694. var d = b.ownerDocument
  1695. var ngIdButton = d.createElement('span')
  1696. ngIdButton.className = 'nrn-contributor-ng-id-button'
  1697. ngIdButton.textContent = '+ID'
  1698. this._initContributorDataset(ngIdButton.dataset)
  1699. var ngNameButton = d.createElement('span')
  1700. ngNameButton.className = 'nrn-contributor-ng-name-button'
  1701. ngNameButton.textContent = '+名'
  1702. this._initContributorDataset(ngNameButton.dataset)
  1703. b.className = 'nrn-user-ng-button'
  1704. b.appendChild(d.createTextNode('['))
  1705. b.appendChild(ngIdButton)
  1706. if (this.contributor.name) {
  1707. b.appendChild(d.createTextNode('/'))
  1708. } else {
  1709. ngNameButton.style.display = 'none'
  1710. }
  1711. b.appendChild(ngNameButton)
  1712. b.appendChild(d.createTextNode(']'))
  1713. },
  1714. get ngId() {
  1715. return this.elem.querySelector('.nrn-contributor-link')
  1716. .classList.contains('nrn-ng-id-contributor-link')
  1717. },
  1718. set ngId(ngId) {
  1719. var a = this.elem.querySelector('.nrn-contributor-link')
  1720. a.classList[ngId ? 'add' : 'remove']('nrn-ng-id-contributor-link')
  1721. var b = this.elem.querySelector('.nrn-contributor-ng-id-button')
  1722. b.textContent = ngId ? 'xID' : '+ID'
  1723. b.dataset.type = ngId ? 'remove' : 'add'
  1724. },
  1725. get ngName() {
  1726. var e = this.elem.querySelector('.nrn-matched-ng-contributor-name')
  1727. return e ? e.textContent : ''
  1728. },
  1729. set ngName(ngName) {
  1730. var b = this.elem.querySelector('.nrn-contributor-ng-name-button')
  1731. b.textContent = ngName ? 'x名' : '+名'
  1732. b.dataset.type = ngName ? 'remove' : 'add'
  1733. b.dataset.matched = ngName
  1734. emphasizeMatchedText(
  1735. this.elem.querySelector('.nrn-contributor-link'),
  1736. ngName,
  1737. function(text) {
  1738. var result = this.elem.ownerDocument.createElement('span')
  1739. result.className = 'nrn-matched-ng-contributor-name'
  1740. result.textContent = text
  1741. return result
  1742. }.bind(this))
  1743. },
  1744. _bindToContributor() {
  1745. this.ngId = this.contributor.ngId
  1746. this.ngName = this.contributor.ngName
  1747. this._listeners.bind(this.contributor)
  1748. return this
  1749. },
  1750. unbind() {
  1751. this._listeners.unbind()
  1752. },
  1753. })
  1754.  
  1755. var ChannelView = function ChannelView(doc, contributor) {
  1756. ContributorView.call(this, doc, contributor)
  1757. this._listeners = new Listeners({ngChanged: set(this, 'ng')})
  1758. this._bindToContributor()
  1759. }
  1760. ChannelView.prototype = createObject(ContributorView.prototype, {
  1761. get _label() {
  1762. return 'チャンネル: '
  1763. },
  1764. _setNgButton(e) {
  1765. e.className = 'nrn-contributor-ng-button'
  1766. e.textContent = '[+]'
  1767. this._initContributorDataset(e.dataset)
  1768. },
  1769. get ng() {
  1770. return this.elem.querySelector('.nrn-contributor-link')
  1771. .classList.contains('nrn-ng-contributor-link')
  1772. },
  1773. set ng(ng) {
  1774. var a = this.elem.querySelector('.nrn-contributor-link')
  1775. a.classList[ng ? 'add' : 'remove']('nrn-ng-contributor-link')
  1776. var b = this.elem.querySelector('.nrn-contributor-ng-button')
  1777. b.textContent = ng ? '[x]' : '[+]'
  1778. b.dataset.type = ng ? 'remove' : 'add'
  1779. },
  1780. _bindToContributor() {
  1781. this.ng = this.contributor.ng
  1782. this._listeners.bind(this.contributor)
  1783. return this
  1784. },
  1785. unbind() {
  1786. this._listeners.unbind()
  1787. },
  1788. })
  1789.  
  1790. ContributorView.new = function(doc, contributor) {
  1791. switch (contributor.type) {
  1792. case 'user': return new UserView(doc, contributor)
  1793. case 'channel': return new ChannelView(doc, contributor)
  1794. default: throw new Error(contributor.type)
  1795. }
  1796. }
  1797. return ContributorView
  1798. })()
  1799.  
  1800. var MovieInfo = (function() {
  1801. var createElem = function(doc) {
  1802. var e = doc.createElement('P')
  1803. e.className = 'nrn-error'
  1804. var t = doc.createElement('p')
  1805. t.className = 'nrn-tag-container'
  1806. var c = doc.createElement('p')
  1807. c.className = 'nrn-contributor-container'
  1808. var result = doc.createElement('div')
  1809. result.className = 'nrn-movie-info-container'
  1810. result.appendChild(e)
  1811. result.appendChild(t)
  1812. result.appendChild(c)
  1813. return result
  1814. }
  1815. var createToggle = function(doc) {
  1816. var result = doc.createElement('span')
  1817. result.className = 'nrn-movie-info-toggle'
  1818. result.textContent = TOGGLE_OPEN_TEXT
  1819. return result
  1820. }
  1821. var MovieInfo = function(doc) {
  1822. this.elem = createElem(doc)
  1823. this.toggle = createToggle(doc)
  1824. this.togglable = true
  1825. this._tagViews = []
  1826. this._contributorView = null
  1827. this._error = Movie.NO_ERROR
  1828. this._actionPane = null
  1829. this._listeners = new Listeners({
  1830. tagsChanged: this._createAndSetTagViews.bind(this),
  1831. contributorChanged: this._createAndSetContributorView.bind(this),
  1832. errorChanged: set(this, 'error'),
  1833. })
  1834. }
  1835. MovieInfo.prototype = {
  1836. set actionPane(actionPane) {
  1837. this._actionPane = actionPane
  1838. this.elem.insertBefore(actionPane.elem, this.elem.firstChild)
  1839. },
  1840. get tagViews() { return this._tagViews },
  1841. set tagViews(tagViews) {
  1842. this._tagViews = tagViews
  1843. var e = this.elem.querySelector('.nrn-tag-container')
  1844. for (var v of tagViews) e.appendChild(v.elem)
  1845. },
  1846. get contributorView() { return this._contributorView },
  1847. set contributorView(contributorView) {
  1848. this._contributorView = contributorView
  1849. this.elem.querySelector('.nrn-contributor-container')
  1850. .appendChild(contributorView.elem)
  1851. },
  1852. get error() { return this._error },
  1853. set error(error) {
  1854. if (this._error === error) return
  1855. this._error = error
  1856. this.elem.querySelector('.nrn-error').textContent = error.message
  1857. },
  1858. hasAny() {
  1859. return Boolean(this.elem.querySelector('.nrn-action-pane')
  1860. || this.elem.querySelector('.nrn-movie-tag')
  1861. || this.elem.querySelector('.nrn-contributor')
  1862. || this.error !== Movie.NO_ERROR)
  1863. },
  1864. _createAndSetTagViews(tags) {
  1865. var d = this.elem.ownerDocument
  1866. this.tagViews = tags.map(function(tag) {
  1867. return new TagView(d, tag).bindToTag(tag)
  1868. })
  1869. },
  1870. _createAndSetContributorView(contributor) {
  1871. if (contributor === Contributor.NULL) return
  1872. var d = this.elem.ownerDocument
  1873. this.contributorView = ContributorView.new(d, contributor)
  1874. },
  1875. bindToMovie(movie) {
  1876. this._createAndSetTagViews(movie.tags)
  1877. this._createAndSetContributorView(movie.contributor)
  1878. this.error = movie.error
  1879. if (!movie.thumbInfoDone) this._listeners.bind(movie)
  1880. },
  1881. unbind() {
  1882. this._listeners.unbind()
  1883. this.tagViews.forEach(function(v) { v.unbind() })
  1884. if (this.contributorView) this.contributorView.unbind()
  1885. if (this._actionPane) this._actionPane.unbind()
  1886. },
  1887. }
  1888. return MovieInfo
  1889. })()
  1890.  
  1891. var Description = (function() {
  1892. var re = /(sm|so|nm|co|ar|im|lv|mylist\/|watch\/|user\/)(?:\d+)/g
  1893. var typeToHRef = {
  1894. sm: 'https://www.nicovideo.jp/watch/',
  1895. so: 'https://www.nicovideo.jp/watch/',
  1896. nm: 'https://www.nicovideo.jp/watch/',
  1897. co: 'https://com.nicovideo.jp/community/',
  1898. ar: 'https://ch.nicovideo.jp/article/',
  1899. im: 'https://seiga.nicovideo.jp/seiga/',
  1900. lv: 'http://live.nicovideo.jp/watch/',
  1901. 'mylist/': 'https://www.nicovideo.jp/',
  1902. 'watch/': 'https://www.nicovideo.jp/',
  1903. 'user/': 'https://www.nicovideo.jp/',
  1904. }
  1905. var createAnchor = function(doc, href, text) {
  1906. var a = doc.createElement('a')
  1907. a.target = '_blank'
  1908. a.href = href
  1909. a.textContent = text
  1910. return a
  1911. }
  1912. var createCloseButton = function(doc) {
  1913. var result = doc.createElement('span')
  1914. result.className = 'nrn-description-close-button'
  1915. result.textContent = TOGGLE_CLOSE_TEXT
  1916. return result
  1917. }
  1918. var createElem = function(doc, closeButton) {
  1919. var text = doc.createElement('span')
  1920. text.className = 'nrn-description-text'
  1921. var result = doc.createElement('p')
  1922. result.className = 'itemDescription ranking nrn-description'
  1923. result.appendChild(text)
  1924. result.appendChild(closeButton)
  1925. return result
  1926. }
  1927. var createOpenButton = function(doc) {
  1928. var result = doc.createElement('span')
  1929. result.className = 'nrn-description-open-button'
  1930. result.textContent = TOGGLE_OPEN_TEXT
  1931. return result
  1932. }
  1933. var Description = function(doc) {
  1934. this.closeButton = createCloseButton(doc)
  1935. this.elem = createElem(doc, this.closeButton)
  1936. this.openButton = createOpenButton(doc)
  1937. this.original = null
  1938. this.text = ''
  1939. this.linkified = false
  1940. this.togglable = true
  1941. this._listeners = new Listeners({
  1942. 'descriptionChanged': set(this, 'text'),
  1943. })
  1944. }
  1945. Description.prototype = {
  1946. linkify() {
  1947. if (this.linkified) return
  1948. this.linkified = true
  1949. var t = this.text
  1950. var d = this.elem.ownerDocument
  1951. var f = d.createDocumentFragment()
  1952. var lastIndex = 0
  1953. for (var r; r = re.exec(t);) {
  1954. f.appendChild(d.createTextNode(t.slice(lastIndex, r.index)))
  1955. f.appendChild(createAnchor(d, typeToHRef[r[1]] + r[0], r[0]))
  1956. lastIndex = re.lastIndex
  1957. }
  1958. f.appendChild(d.createTextNode(t.slice(lastIndex)))
  1959. f.normalize()
  1960. this.elem.firstChild.appendChild(f)
  1961. },
  1962. bindToMovie(movie) {
  1963. this.text = movie.description
  1964. this._listeners.bind(movie)
  1965. },
  1966. unbind() {
  1967. this._listeners.unbind()
  1968. },
  1969. }
  1970. return Description
  1971. })()
  1972.  
  1973. var MovieRoot = (function() {
  1974. var MovieRoot = function(elem) {
  1975. this.elem = elem
  1976. var d = elem.ownerDocument
  1977. this.movieInfo = new MovieInfo(d)
  1978. this.description = new Description(d)
  1979. this._openNewWindow = false
  1980. this.movieTitle = null
  1981. this._movieListeners = new Listeners({
  1982. thumbInfoDone: this.setThumbInfoDone.bind(this),
  1983. })
  1984. this._movieViewModeListeners = new Listeners({
  1985. changed: set(this, 'viewMode'),
  1986. })
  1987. this._configOpenNewWindowListeners = new Listeners({
  1988. changed: set(this, 'openNewWindow'),
  1989. })
  1990. }
  1991. MovieRoot.prototype = {
  1992. markMovieAnchor() {
  1993. for (var a of this._movieAnchors) a.dataset.nrnMovieAnchor = 'true'
  1994. },
  1995. set id(id) {
  1996. for (var a of this._movieAnchors) a.dataset.nrnMovieId = id
  1997. },
  1998. get titleElem() {
  1999. throw new Error('must be implemented')
  2000. },
  2001. set title(title) {
  2002. this.titleElem.textContent = title
  2003. for (var a of this._movieAnchors) a.dataset.nrnMovieTitle = title
  2004. },
  2005. get _reduced() {
  2006. return this.elem.classList.contains('nrn-reduce')
  2007. },
  2008. _halfThumb() {},
  2009. _restoreThumb() {},
  2010. _reduce() {
  2011. this.elem.classList.add('nrn-reduce')
  2012. this._halfThumb()
  2013. },
  2014. _unreduce() {
  2015. this.elem.classList.remove('nrn-reduce')
  2016. this._restoreThumb()
  2017. },
  2018. get _hidden() {
  2019. return this.elem.classList.contains('nrn-hide')
  2020. },
  2021. _hide() {
  2022. this.elem.classList.add('nrn-hide')
  2023. },
  2024. _show() {
  2025. this.elem.classList.remove('nrn-hide')
  2026. },
  2027. get viewMode() {
  2028. if (this.elem.classList.contains('nrn-reduce')) return 'reduce'
  2029. if (this.elem.classList.contains('nrn-hide')) return 'hide'
  2030. return 'doNothing'
  2031. },
  2032. set viewMode(viewMode) {
  2033. if (this._reduced) this._unreduce()
  2034. else if (this._hidden) this._show()
  2035. switch (viewMode) {
  2036. case 'reduce': this._reduce(); break
  2037. case 'hide': this._hide(); break
  2038. case 'doNothing': break
  2039. default: throw new Error(viewMode)
  2040. }
  2041. },
  2042. get _movieAnchorSelectors() {
  2043. throw new Error('must be implemented')
  2044. },
  2045. get _movieAnchors() {
  2046. var result = []
  2047. for (var s of this._movieAnchorSelectors) {
  2048. var a = this.elem.querySelector(s)
  2049. if (a) result.push(a)
  2050. }
  2051. return result
  2052. },
  2053. get openNewWindow() { return this._openNewWindow },
  2054. set openNewWindow(openNewWindow) {
  2055. this._openNewWindow = openNewWindow
  2056. var t = openNewWindow ? '_blank' : ''
  2057. for (var a of this._movieAnchors) a.target = t
  2058. },
  2059. get _movieInfoVisible() {
  2060. return Boolean(this.movieInfo.elem.parentNode)
  2061. },
  2062. set _movieInfoVisible(visible) {
  2063. if (visible) {
  2064. this._addMovieInfo()
  2065. this.movieInfo.toggle.textContent = TOGGLE_CLOSE_TEXT
  2066. } else {
  2067. this.movieInfo.elem.remove()
  2068. this.movieInfo.toggle.textContent = TOGGLE_OPEN_TEXT
  2069. }
  2070. },
  2071. toggleMovieInfo() {
  2072. this._movieInfoVisible = !this._movieInfoVisible
  2073. },
  2074. set actionPane(actionPane) {
  2075. this.movieInfo.actionPane = actionPane
  2076. },
  2077. _addMovieInfo() {
  2078. throw new Error('must be implemented')
  2079. },
  2080. _addMovieInfoToggle() {
  2081. this.elem.querySelector('.itemData')
  2082. .appendChild(this.movieInfo.toggle)
  2083. },
  2084. setMovieInfoToggleIfRequired() {},
  2085. _updateByMovieInfoTogglable() {
  2086. if (!this.movieInfo.hasAny()) return
  2087. if (this.movieInfo.togglable) {
  2088. this._addMovieInfoToggle()
  2089. } else {
  2090. this.movieInfo.toggle.remove()
  2091. }
  2092. this._movieInfoVisible = !this.movieInfo.togglable
  2093. },
  2094. get movieInfoTogglable() {
  2095. return this.movieInfo.togglable
  2096. },
  2097. set movieInfoTogglable(movieInfoTogglable) {
  2098. this.movieInfo.togglable = movieInfoTogglable
  2099. this._updateByMovieInfoTogglable()
  2100. },
  2101. _queryOriginalDescriptionElem() {
  2102. return this.elem.querySelector('.itemDescription')
  2103. },
  2104. get _originalDescriptionElem() {
  2105. var result = this.description.original
  2106. if (!result) {
  2107. result
  2108. = this.description.original
  2109. = this._queryOriginalDescriptionElem()
  2110. }
  2111. return result
  2112. },
  2113. get _descriptionExpanded() {
  2114. return Boolean(this.description.elem.parentNode)
  2115. },
  2116. set _descriptionExpanded(expanded) {
  2117. var o = this._originalDescriptionElem
  2118. var d = this.description
  2119. if (expanded && o.parentNode) {
  2120. d.linkify()
  2121. o.parentNode.replaceChild(d.elem, o)
  2122. } else if (!expanded && d.elem.parentNode) {
  2123. d.elem.parentNode.replaceChild(o, d.elem)
  2124. }
  2125. },
  2126. _updateByDescriptionTogglable() {
  2127. if (!this.description.text) return
  2128. if (this.description.togglable) {
  2129. this._originalDescriptionElem?.appendChild(this.description.openButton)
  2130. this.description.elem.appendChild(this.description.closeButton)
  2131. } else {
  2132. this.description.closeButton.remove()
  2133. }
  2134. this._descriptionExpanded = !this.description.togglable
  2135. },
  2136. toggleDescription() {
  2137. this._descriptionExpanded = !this._descriptionExpanded
  2138. },
  2139. get descriptionTogglable() {
  2140. return this.description.togglable
  2141. },
  2142. set descriptionTogglable(descriptionTogglable) {
  2143. this.description.togglable = descriptionTogglable
  2144. this._updateByDescriptionTogglable()
  2145. },
  2146. setThumbInfoDone() {
  2147. this.elem.classList.add('nrn-thumb-info-done')
  2148. },
  2149. get thumbInfoDone() {
  2150. return this.elem.classList.contains('nrn-thumb-info-done')
  2151. },
  2152. bindToMovie(movie) {
  2153. this.movieInfo.bindToMovie(movie)
  2154. this.description.bindToMovie(movie)
  2155. if (movie.thumbInfoDone) this.setThumbInfoDone()
  2156. else this._movieListeners.bind(movie)
  2157. },
  2158. bindToMovieViewMode(movieViewMode) {
  2159. this.viewMode = movieViewMode.value
  2160. this._movieViewModeListeners.bind(movieViewMode)
  2161. },
  2162. bindToConfig(config) {
  2163. this.openNewWindow = config.openNewWindow.value
  2164. this._configOpenNewWindowListeners.bind(config.openNewWindow)
  2165. },
  2166. unbind() {
  2167. this.movieInfo.unbind()
  2168. this.description.unbind()
  2169. this._movieListeners.unbind()
  2170. this._movieViewModeListeners.unbind()
  2171. this._configOpenNewWindowListeners.unbind()
  2172. if (this.movieTitle) this.movieTitle.unbind()
  2173. },
  2174. }
  2175. return MovieRoot
  2176. })()
  2177.  
  2178. var ConfigBar = (function() {
  2179. var createConfigBar = function(doc) {
  2180. var html = `<div id=nrn-config-bar>
  2181. <label>
  2182. 閲覧済みの動画を
  2183. <select id=nrn-visited-movie-view-mode-select>
  2184. <option value=reduce>縮小</option>
  2185. <option value=hide>非表示</option>
  2186. <option value=doNothing>通常表示</option>
  2187. </select>
  2188. </label>
  2189. |
  2190. <label>
  2191. 投稿者
  2192. <select id=nrn-visible-contributor-type-select>
  2193. <option value=all>全部</option>
  2194. <option value=user>ユーザー</option>
  2195. <option value=channel>チャンネル</option>
  2196. </select>
  2197. </label>
  2198. |
  2199. <label><input type=checkbox id=nrn-ng-movie-visible-checkbox> NG動画を表示</label>
  2200. |
  2201. <span id=nrn-config-button>設定</span>
  2202. </div>`
  2203. var e = doc.createElement('div')
  2204. e.innerHTML = html
  2205. var result = e.firstChild
  2206. result.remove()
  2207. return result
  2208. }
  2209. var ConfigBar = function(doc) {
  2210. this.elem = createConfigBar(doc)
  2211. }
  2212. ConfigBar.prototype = {
  2213. get _viewModeSelect() {
  2214. return this.elem.querySelector('#nrn-visited-movie-view-mode-select')
  2215. },
  2216. get visitedMovieViewMode() {
  2217. return this._viewModeSelect.value
  2218. },
  2219. set visitedMovieViewMode(viewMode) {
  2220. this._viewModeSelect.value = viewMode
  2221. },
  2222. get _visibleContributorTypeSelect() {
  2223. return this.elem.querySelector('#nrn-visible-contributor-type-select')
  2224. },
  2225. get visibleContributorType() {
  2226. return this._visibleContributorTypeSelect.value
  2227. },
  2228. set visibleContributorType(type) {
  2229. this._visibleContributorTypeSelect.value = type
  2230. },
  2231. bindToConfig(config) {
  2232. this.visitedMovieViewMode = config.visitedMovieViewMode.value
  2233. this.visibleContributorType = config.visibleContributorType.value
  2234. config.visitedMovieViewMode.on('changed', set(this, 'visitedMovieViewMode'))
  2235. config.visibleContributorType.on('changed', set(this, 'visibleContributorType'))
  2236. return this
  2237. },
  2238. }
  2239. return ConfigBar
  2240. })()
  2241.  
  2242. var NicoPage = function(doc) {
  2243. this.doc = doc
  2244. this._toggleToMovieRoot = new Map()
  2245. }
  2246. NicoPage.prototype = {
  2247. createConfigBar() {
  2248. return new ConfigBar(this.doc)
  2249. },
  2250. createTables() { return [] },
  2251. createMovieRoot() {
  2252. throw new Error('must be implemented')
  2253. },
  2254. get _configBarContainer() {
  2255. throw new Error('must be implemented')
  2256. },
  2257. addConfigBar(bar) {
  2258. var target = this._configBarContainer
  2259. target.insertBefore(bar.elem, target.firstChild)
  2260. },
  2261. parse() {
  2262. throw new Error('must be implemented')
  2263. },
  2264. mapToggleTo(movieRoot) {
  2265. var m = this._toggleToMovieRoot
  2266. m.set(movieRoot.movieInfo.toggle, movieRoot)
  2267. m.set(movieRoot.description.openButton, movieRoot)
  2268. m.set(movieRoot.description.closeButton, movieRoot)
  2269. },
  2270. unmapToggleFrom(movieRoot) {
  2271. var m = this._toggleToMovieRoot
  2272. m.delete(movieRoot.movieInfo.toggle)
  2273. m.delete(movieRoot.description.openButton)
  2274. m.delete(movieRoot.description.closeButton)
  2275. },
  2276. getMovieRootBy(toggle) {
  2277. return this._toggleToMovieRoot.get(toggle)
  2278. },
  2279. _configDialogLoaded() {},
  2280. showConfigDialog(config) {
  2281. var back = this.doc.createElement('div')
  2282. back.style.backgroundColor = 'black'
  2283. back.style.opacity = '0.5'
  2284. back.style.zIndex = '10000'
  2285. back.style.position = 'fixed'
  2286. back.style.top = '0'
  2287. back.style.left = '0'
  2288. back.style.width = '100%'
  2289. back.style.height = '100%'
  2290. this.doc.body.appendChild(back)
  2291.  
  2292. var f = this.doc.createElement('iframe')
  2293. f.style.position = 'fixed'
  2294. f.style.top = '0'
  2295. f.style.left = '0'
  2296. f.style.width = '100%'
  2297. f.style.height = '100%'
  2298. f.style.zIndex = '10001'
  2299. f.srcdoc = ConfigDialog.SRCDOC
  2300. f.addEventListener('load', function loaded() {
  2301. this._configDialogLoaded(f.contentDocument)
  2302. const openInTab = typeof GM_openInTab === 'undefined'
  2303. ? GM.openInTab : GM_openInTab
  2304. new ConfigDialog(config, f.contentDocument, openInTab)
  2305. .on('closed', function() {
  2306. f.remove()
  2307. back.remove()
  2308. })
  2309. }.bind(this))
  2310. this.doc.body.appendChild(f)
  2311. },
  2312. bindToConfig() {},
  2313. get _pendingMoviesInvisibleCss() {
  2314. throw new Error('must be implemented')
  2315. },
  2316. _createPendingMoviesInvisibleStyle() {
  2317. var result = this.doc.createElement('style')
  2318. result.id = 'nrn-pending-movies-hide-style'
  2319. result.textContent = this._pendingMoviesInvisibleCss
  2320. return result
  2321. },
  2322. set pendingMoviesVisible(v) {
  2323. var id = 'nrn-pending-movies-hide-style'
  2324. if (v) {
  2325. this.doc.getElementById(id).remove()
  2326. } else {
  2327. if (!this.doc.head) {
  2328. new MutationObserver((recs, observer) => {
  2329. if (!this.doc.head) return;
  2330. this.doc.head.appendChild(this._createPendingMoviesInvisibleStyle());
  2331. observer.disconnect();
  2332. }).observe(this.doc, {childList: true, subtree: true});
  2333. } else {
  2334. this.doc.head.appendChild(this._createPendingMoviesInvisibleStyle());
  2335. }
  2336. }
  2337. },
  2338. get css() {
  2339. throw new Error('must be implemented')
  2340. },
  2341. observeMutation() {},
  2342. }
  2343. Object.assign(NicoPage, {
  2344. MovieTitle,
  2345. ActionPane,
  2346. TagView,
  2347. ContributorView,
  2348. MovieInfo,
  2349. Description,
  2350. MovieRoot,
  2351. ConfigBar,
  2352. })
  2353. return NicoPage
  2354. })()
  2355.  
  2356. var ListPage = (function(_super) {
  2357.  
  2358. var MovieRoot = (function(_super) {
  2359. var MovieRoot = function(elem) {
  2360. _super.call(this, elem)
  2361. }
  2362. MovieRoot.prototype = createObject(_super.prototype, {
  2363. get titleElem() {
  2364. return this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObjectTitle.NC-VideoMediaObject-title')
  2365. },
  2366. get _movieAnchorSelectors() {
  2367. return ['.NC-MediaObject.NC-VideoMediaObject .NC-Link.NC-MediaObject-contents']
  2368. },
  2369. set actionPane(actionPane) {
  2370. this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObject-main')?.appendChild(actionPane.elem)
  2371. },
  2372. _addMovieInfo() {
  2373. this.elem.appendChild(this.movieInfo.elem)
  2374. },
  2375. setMovieInfoToggleIfRequired() {
  2376. if (!this.movieInfo.toggle.parentNode) {
  2377. this.elem.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObject-action')?.appendChild(this.movieInfo.toggle)
  2378. }
  2379. },
  2380. })
  2381. return MovieRoot
  2382. })(_super.MovieRoot)
  2383.  
  2384. var SubMovieRoot = (function(_super) {
  2385. var SubMovieRoot = function(elem) {
  2386. _super.call(this, elem)
  2387. elem.classList.add('nrn-sub-movie-root')
  2388. }
  2389. SubMovieRoot.prototype = createObject(_super.prototype, {
  2390. get titleElem() {
  2391. return this.elem.querySelector('.RankingSubVideo-title')
  2392. },
  2393. get _movieAnchorSelectors() {
  2394. return ['.RankingSubVideo-title', '.RankingSubVideo-thumbnail']
  2395. },
  2396. set actionPane(actionPane) {
  2397. this.movieInfo.actionPane = actionPane
  2398. },
  2399. _addMovieInfo() {
  2400. this.elem.appendChild(this.movieInfo.elem)
  2401. },
  2402. setMovieInfoToggleIfRequired() {
  2403. if (!this.movieInfo.toggle.parentNode) {
  2404. this.elem.appendChild(this.movieInfo.toggle)
  2405. }
  2406. },
  2407. })
  2408. return SubMovieRoot
  2409. })(_super.MovieRoot)
  2410.  
  2411. var AdMovieRoot = (function(_super) {
  2412. var AdMovieRoot = function(elem) {
  2413. _super.call(this, elem)
  2414. elem.classList.add('nrn-ad-movie-root')
  2415. }
  2416. AdMovieRoot.prototype = createObject(_super.prototype, {
  2417. get titleElem() {
  2418. return this.elem.querySelector('.NC-MediaObjectTitle.RankingMainNicoad')
  2419. },
  2420. get _movieAnchorSelectors() {
  2421. return ['.NC-Link.NC-MediaObject-contents']
  2422. },
  2423. set actionPane(actionPane) {
  2424. this.elem.querySelector('.NC-MediaObject-main').appendChild(actionPane.elem)
  2425. },
  2426. _addMovieInfo() {
  2427. this.elem.appendChild(this.movieInfo.elem)
  2428. },
  2429. setMovieInfoToggleIfRequired() {
  2430. if (!this.movieInfo.toggle.parentNode) {
  2431. this.elem.querySelector('.NC-MediaObject-action').appendChild(this.movieInfo.toggle)
  2432. }
  2433. },
  2434. })
  2435. return AdMovieRoot
  2436. })(_super.MovieRoot)
  2437.  
  2438. var parent = function(className, child) {
  2439. for (var e = child; e; e = e.parentNode) {
  2440. if (e.classList.contains(className)) return e
  2441. }
  2442. return null
  2443. }
  2444. var ListPage = function(doc) {
  2445. _super.call(this, doc)
  2446. }
  2447. ListPage.prototype = createObject(_super.prototype, {
  2448. createTables() { return [] },
  2449. createMovieRoot(resultOfParsing) {
  2450. switch (resultOfParsing.type) {
  2451. case 'main': return new MovieRoot(resultOfParsing.rootElem)
  2452. case 'sub': return new SubMovieRoot(resultOfParsing.rootElem)
  2453. case 'ad': return new AdMovieRoot(resultOfParsing.rootElem)
  2454. default: throw new Error(resultOfParsing.type)
  2455. }
  2456. },
  2457. get _configBarContainer() {
  2458. return this.doc.querySelector('.RankingVideoListContainer')
  2459. },
  2460. parse(target) {
  2461. target = target || this.doc
  2462. return this._parseMain(target).concat(this._parseSub(target))
  2463. },
  2464. _parseMain(target) {
  2465. return Array.from(target.querySelectorAll('.NC-VideoMediaObjectWrapper'))
  2466. .map(function(item) {
  2467. const ncLink = item.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-Link');
  2468. const id = ncLink ? movieIdOf(ncLink.href) : null;
  2469. return {
  2470. type: 'main',
  2471. movie: {
  2472. id,
  2473. title: item.querySelector('.NC-MediaObject.NC-VideoMediaObject .NC-MediaObjectTitle.NC-VideoMediaObject-title')?.textContent?.trim() ?? "",
  2474. },
  2475. rootElem: item,
  2476. }
  2477. }).filter(e => Boolean(e.movie.id));
  2478. },
  2479. _parseSub(target) {
  2480. var selector = '.HotTopicVideosContainer > .BaseRankingContentContainer-main > .RankingSubVideo'
  2481. + ', .SpecifiedGenreRankingVideosContainer-subVideos > .RankingSubVideo'
  2482. return Array.from(target.querySelectorAll(selector))
  2483. .map(function(rootElem) {
  2484. return {
  2485. type: 'sub',
  2486. movie: {
  2487. id: rootElem.querySelector('.RankingSubVideo-thumbnail').pathname.slice('/watch/'.length),
  2488. title: rootElem.querySelector('.Thumbnail.RankingSubVideo .Thumbnail-image').getAttribute('alt').trim(),
  2489. },
  2490. rootElem,
  2491. }
  2492. })
  2493. },
  2494. _configDialogLoaded(doc) {
  2495. doc.getElementById('togglable').hidden = true
  2496. },
  2497. observeMutation(callback) {
  2498. const nodeList = document.querySelectorAll('.NC-MediaObject.NC-NicoadMediaObject.RankingMainNicoad')
  2499. for (const node of Array.from(nodeList)) {
  2500. new MutationObserver((records, observer) => {
  2501. const {target} = records[0]
  2502. if (!target.dataset.contentId) return
  2503. const titleElem = target.querySelector('.NC-MediaObjectTitle.RankingMainNicoad')
  2504. if (!titleElem) return
  2505. observer.disconnect()
  2506. const id = target.dataset.contentId
  2507. const title = titleElem.textContent.trim()
  2508. callback([{
  2509. type: 'ad',
  2510. movie: {id, title},
  2511. rootElem: target.parentNode,
  2512. }], true)
  2513. }).observe(node, {
  2514. attributes: true,
  2515. attributeFilter: ['data-content-id'],
  2516. })
  2517. }
  2518. },
  2519. get _pendingMoviesInvisibleCss() {
  2520. return `.NC-VideoMediaObjectWrapper,
  2521. .MediaObject.RankingSubVideo,
  2522. .NC-NicoadMediaObjectWrapper {
  2523. visibility: hidden;
  2524. }
  2525. .NC-VideoMediaObjectWrapper[data-sensitive],
  2526. .NC-VideoMediaObjectWrapper.nrn-thumb-info-done,
  2527. .MediaObject.RankingSubVideo.nrn-thumb-info-done,
  2528. .NC-NicoadMediaObjectWrapper.nrn-thumb-info-done,
  2529. .NC-NicoadMediaObjectWrapper[data-blank] {
  2530. visibility: inherit;
  2531. }
  2532. `
  2533. },
  2534. get css() {
  2535. return `#nrn-config-button,
  2536. .nrn-visit-button:hover,
  2537. .nrn-movie-ng-button:hover,
  2538. .nrn-title-ng-button:hover,
  2539. .nrn-tag-ng-button:hover,
  2540. .nrn-contributor-ng-button:hover,
  2541. .nrn-contributor-ng-id-button:hover,
  2542. .nrn-contributor-ng-name-button:hover,
  2543. .nrn-movie-info-toggle:hover {
  2544. text-decoration: underline;
  2545. cursor: pointer;
  2546. }
  2547. .nrn-movie-tag {
  2548. display: inline-block;
  2549. margin-right: 1em;
  2550. }
  2551. .nrn-movie-tag-link,
  2552. .nrn-contributor-link {
  2553. color: #333333;
  2554. }
  2555. .nrn-movie-tag-link.nrn-movie-ng-tag-link,
  2556. .nrn-contributor-link.nrn-ng-contributor-link,
  2557. .nrn-matched-ng-contributor-name,
  2558. .nrn-matched-ng-title {
  2559. color: white;
  2560. background-color: fuchsia;
  2561. }
  2562. .NC-MediaObject.RankingMainNicoad.nrn-thumb-info-done.nrn-reduce .RankingMainNicoad-meta {
  2563. border-top: 0px;
  2564. }
  2565. .nrn-movie-info-container .nrn-tag-container,
  2566. .nrn-movie-info-container .nrn-contributor-container {
  2567. line-height: 1.5em;
  2568. margin-top: 4px;
  2569. }
  2570. .nrn-hide,
  2571. .NC-VideoMediaObjectWrapper.nrn-reduce .RankingMainVideo .RankingMainVideo-description,
  2572. .RankingMainNicoad.nrn-reduce .RankingMainNicoad-message div[data-nicoad-message],
  2573. .BaseRankingContentContainer.SpecifiedGenreRankingVideosContainer .SpecifiedGenreRankingVideosContainer-subVideo.nrn-hide {
  2574. display: none;
  2575. }
  2576. .NC-VideoMediaObjectWrapper.nrn-reduce .RankingMainVideo .NC-VideoMediaObject-thumbnail,
  2577. .NC-NicoadMediaObjectWrapper.nrn-reduce .RankingMainNicoad .NC-NicoadMediaObject-thumbnail {
  2578. width: 130px;
  2579. }
  2580. .nrn-user-ng-button {
  2581. display: inline-block;
  2582. }
  2583. .NC-MediaObject.RankingMainVideo .NC-VideoMediaObject-title.nrn-ng-movie-title,
  2584. .nrn-contributor-link.nrn-ng-id-contributor-link,
  2585. .MediaObject.RankingSubVideo .RankingSubVideo-title.nrn-ng-movie-title,
  2586. .NC-MediaObject.RankingMainNicoad .NC-MediaObjectTitle.nrn-ng-movie-title {
  2587. text-decoration: line-through;
  2588. }
  2589. .RankingMainNicoad {
  2590. position: relative;
  2591. }
  2592. .RankingMainVideo .nrn-action-pane,
  2593. .RankingMainNicoad .nrn-action-pane {
  2594. display: none;
  2595. position: absolute;
  2596. top: 0px;
  2597. right: 35px;
  2598. padding: 3px;
  2599. color: #999;
  2600. background-color: rgb(105, 105, 105);
  2601. z-index: 11;
  2602. }
  2603. .RankingMainVideo:hover .nrn-action-pane,
  2604. .RankingMainNicoad:hover .nrn-action-pane {
  2605. display: block;
  2606. }
  2607. .RankingMainVideo:hover .nrn-action-pane .nrn-visit-button,
  2608. .RankingMainVideo:hover .nrn-action-pane .nrn-movie-ng-button,
  2609. .RankingMainVideo:hover .nrn-action-pane .nrn-title-ng-button,
  2610. .RankingMainNicoad:hover .nrn-action-pane .nrn-visit-button,
  2611. .RankingMainNicoad:hover .nrn-action-pane .nrn-movie-ng-button,
  2612. .RankingMainNicoad:hover .nrn-action-pane .nrn-title-ng-button {
  2613. color: white;
  2614. }
  2615. .RankingMainVideo:hover .nrn-action-pane .nrn-movie-ng-button,
  2616. .RankingMainVideo:hover .nrn-action-pane .nrn-title-ng-button,
  2617. .RankingMainNicoad:hover .nrn-action-pane .nrn-movie-ng-button,
  2618. .RankingMainNicoad:hover .nrn-action-pane .nrn-title-ng-button {
  2619. margin-left: 5px;
  2620. border-left: solid thin;
  2621. padding-left: 5px;
  2622. }
  2623. .NC-MediaObject.RankingMainNicoad.nrn-thumb-info-done .RankingMainNicoad-point {
  2624. right: 2em;
  2625. }
  2626. .RankingMainVideo .nrn-movie-info-toggle,
  2627. .RankingMainNicoad .nrn-movie-info-toggle {
  2628. display: block;
  2629. color: #999;
  2630. text-align: center;
  2631. }
  2632. .nrn-sub-movie-root {
  2633. position: relative;
  2634. }
  2635. .nrn-sub-movie-root .nrn-movie-info-toggle {
  2636. display: block;
  2637. text-align: right;
  2638. background-color: white;
  2639. }
  2640. .nrn-sub-movie-root .nrn-movie-info-container {
  2641. display: block;
  2642. position: static;
  2643. padding-top: 5px;
  2644. }
  2645. .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button,
  2646. .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button,
  2647. .nrn-sub-movie-root .nrn-action-pane .nrn-title-ng-button {
  2648. display: inline-block;
  2649. color: #333333;
  2650. }
  2651. .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button,
  2652. .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button {
  2653. margin-right: 0.5em;
  2654. }
  2655. .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-toggle {
  2656. position: absolute;
  2657. right: 0px;
  2658. bottom: 0px;
  2659. padding: 1em 0 0 1em;
  2660. background: rgba(255, 255, 255, 0);
  2661. }
  2662. .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-container {
  2663. display: none;
  2664. position: absolute;
  2665. top: 60px;
  2666. background-color: lightgray;
  2667. z-index: 100;
  2668. padding-left: 5px;
  2669. }
  2670. .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root:hover .nrn-movie-info-container,
  2671. .SpecifiedGenreRankingVideosContainer-subVideo.nrn-sub-movie-root .nrn-movie-info-container:hover {
  2672. display: block;
  2673. }
  2674. .nrn-error {
  2675. color: red;
  2676. }
  2677. `
  2678. },
  2679. })
  2680. Object.assign(ListPage, {
  2681. MovieRoot,
  2682. SubMovieRoot,
  2683. })
  2684. return ListPage
  2685. })(NicoPage)
  2686.  
  2687. var SearchPage = (function(_super) {
  2688.  
  2689. var AbstractMovieRoot = (function(_super) {
  2690. var AbstractMovieRoot = function(elem) {
  2691. _super.call(this, elem)
  2692. }
  2693. AbstractMovieRoot.prototype = createObject(_super.prototype, {
  2694. get titleElem() {
  2695. return this.elem.querySelector('.itemTitle a')
  2696. },
  2697. get _movieAnchorSelectors() {
  2698. return ['.itemTitle a', '.itemThumbWrap']
  2699. },
  2700. })
  2701. return AbstractMovieRoot
  2702. })(_super.MovieRoot)
  2703.  
  2704. var FixedThumbMovieRoot = (function(_super) {
  2705. var FixedThumbMovieRoot = function(elem) {
  2706. _super.call(this, elem)
  2707. }
  2708. FixedThumbMovieRoot.prototype = createObject(_super.prototype, {
  2709. _getThumbElement() {
  2710. const e = this.elem.querySelector('.thumb')
  2711. return e ? e : this.elem.querySelector('.backgroundThumbnail')
  2712. },
  2713. _halfThumb() {
  2714. var e = this._getThumbElement()
  2715. if (!e) return
  2716. var s = e.style
  2717. if (!s.marginTop) return
  2718. s.marginTop = '-9px'
  2719. s.width = '80px'
  2720. s.height = '63px'
  2721. },
  2722. _restoreThumb() {
  2723. var e = this._getThumbElement()
  2724. if (!e) return
  2725. var s = e.style
  2726. if (!s.marginTop) return
  2727. s.marginTop = '-15px'
  2728. s.width = '160px'
  2729. s.height = ''
  2730. },
  2731. })
  2732. return FixedThumbMovieRoot
  2733. })(AbstractMovieRoot)
  2734.  
  2735. var TwoColumnMovieRoot = (function(_super) {
  2736. var TwoColumnMovieRoot = function(elem) {
  2737. _super.call(this, elem)
  2738. }
  2739. TwoColumnMovieRoot.prototype = createObject(_super.prototype, {
  2740. set actionPane(actionPane) {
  2741. this.elem.appendChild(actionPane.elem)
  2742. },
  2743. _addMovieInfo() {
  2744. this.elem.appendChild(this.movieInfo.elem)
  2745. },
  2746. setThumbInfoDone() {
  2747. _super.prototype.setThumbInfoDone.call(this)
  2748. this._updateByMovieInfoTogglable()
  2749. this._updateByDescriptionTogglable()
  2750. },
  2751. })
  2752. return TwoColumnMovieRoot
  2753. })(FixedThumbMovieRoot)
  2754.  
  2755. var FourColumnMovieRoot = (function(_super) {
  2756. var FourColumnMovieRoot = function(elem) {
  2757. _super.call(this, elem)
  2758. elem.classList.add('nrn-4-column-item')
  2759. }
  2760. FourColumnMovieRoot.prototype = createObject(_super.prototype, {
  2761. set actionPane(actionPane) {
  2762. this.movieInfo.actionPane = actionPane
  2763. },
  2764. _addMovieInfo() {
  2765. this.elem.appendChild(this.movieInfo.elem)
  2766. },
  2767. setMovieInfoToggleIfRequired() {
  2768. if (!this.movieInfo.toggle.parentNode) {
  2769. this.elem.appendChild(this.movieInfo.toggle)
  2770. }
  2771. },
  2772. })
  2773. return FourColumnMovieRoot
  2774. })(FixedThumbMovieRoot)
  2775.  
  2776. var MovieRoot = (function(_super) {
  2777. var MovieRoot = function(elem) {
  2778. _super.call(this, elem)
  2779. }
  2780. MovieRoot.prototype = createObject(_super.prototype, {
  2781. set actionPane(actionPane) {
  2782. this.elem.appendChild(actionPane.elem)
  2783. },
  2784. _addMovieInfo() {
  2785. this.elem.querySelector('.itemContent')
  2786. .appendChild(this.movieInfo.elem)
  2787. },
  2788. setThumbInfoDone() {
  2789. _super.prototype.setThumbInfoDone.call(this)
  2790. this._updateByMovieInfoTogglable()
  2791. this._updateByDescriptionTogglable()
  2792. },
  2793. bindToConfig(config) {
  2794. _super.prototype.bindToConfig.call(this, config)
  2795. this.movieInfoTogglable = config.movieInfoTogglable.value
  2796. config.movieInfoTogglable.on('changed', set(this, 'movieInfoTogglable'))
  2797. this.descriptionTogglable = config.descriptionTogglable.value
  2798. config.descriptionTogglable.on('changed', set(this, 'descriptionTogglable'))
  2799. },
  2800. })
  2801. return MovieRoot
  2802. })(FixedThumbMovieRoot)
  2803.  
  2804. var SubMovieRoot = (function(_super) {
  2805. var SubMovieRoot = function(elem) {
  2806. _super.call(this, elem)
  2807. elem.classList.add('nrn-sub-movie-root')
  2808. }
  2809. SubMovieRoot.prototype = createObject(_super.prototype, {
  2810. set actionPane(actionPane) {
  2811. this.movieInfo.actionPane = actionPane
  2812. },
  2813. _addMovieInfo() {
  2814. this.elem.appendChild(this.movieInfo.elem)
  2815. },
  2816. setMovieInfoToggleIfRequired() {
  2817. if (!this.movieInfo.toggle.parentNode) {
  2818. this.elem.appendChild(this.movieInfo.toggle)
  2819. }
  2820. },
  2821. })
  2822. return SubMovieRoot
  2823. })(AbstractMovieRoot)
  2824.  
  2825. var createMainMovieRoot = function(rootElem) {
  2826. var singleColumnView = Boolean(rootElem.getElementsByClassName('videoList01Wrap').length)
  2827. if (singleColumnView) return new MovieRoot(rootElem)
  2828. var twoColumnView = Boolean(rootElem.getElementsByClassName('videoList02Wrap').length)
  2829. if (twoColumnView) return new TwoColumnMovieRoot(rootElem)
  2830. return new FourColumnMovieRoot(rootElem)
  2831. }
  2832. var SearchPage = function(doc) {
  2833. _super.call(this, doc)
  2834. }
  2835. SearchPage.prototype = createObject(_super.prototype, {
  2836. removeEmbeddedStyle() {
  2837. const nodeList = document.querySelectorAll('.itemContent[style="visibility: visible;"]');
  2838. for (const node of Array.from(nodeList)) {
  2839. node.style.visibility = '';
  2840. }
  2841. },
  2842. parse(target) {
  2843. target = target || this.doc
  2844. return this._parseMain(target).concat(this._parseSub(target))
  2845. },
  2846. _parseItem(item) {
  2847. return {
  2848. type: 'main',
  2849. movie: {
  2850. id: item.dataset.videoId,
  2851. title: item.querySelector('.itemTitle a').title,
  2852. },
  2853. rootElem: item,
  2854. }
  2855. },
  2856. parseAutoPagerizedNodes(target) {
  2857. return [this._parseItem(target)]
  2858. },
  2859. _parseMain(target) {
  2860. return Array.from(target.querySelectorAll('.contentBody.video.uad .item[data-video-item]'))
  2861. .map(item => this._parseItem(item))
  2862. },
  2863. _parseSub(target) {
  2864. return Array.from(target.querySelectorAll('#tsukuaso .item'))
  2865. .map(function(item) {
  2866. return {
  2867. type: 'sub',
  2868. movie: {
  2869. id: item.querySelector('.itemThumb').dataset.id,
  2870. title: item.querySelector('.itemTitle a').textContent,
  2871. },
  2872. rootElem: item,
  2873. }
  2874. })
  2875. },
  2876. get _configBarContainer() {
  2877. return this.doc.querySelector('.column.main')
  2878. },
  2879. createMovieRoot(resultOfParsing) {
  2880. switch (resultOfParsing.type) {
  2881. case 'main':
  2882. case 'ad':
  2883. return createMainMovieRoot(resultOfParsing.rootElem)
  2884. case 'sub':
  2885. return new SubMovieRoot(resultOfParsing.rootElem)
  2886. default:
  2887. throw new Error(resultOfParsing.type)
  2888. }
  2889. },
  2890. observeMutation(callback) {
  2891. const nodeList = document.querySelectorAll('.contentBody.video.uad .item.nicoadVideoItem .itemContent')
  2892. for (const node of Array.from(nodeList)) {
  2893. new MutationObserver((records, observer) => {
  2894. for (const r of records) {
  2895. if (SearchPage._isGettingAdDone(r)) {
  2896. observer.disconnect()
  2897. r.target.style.visibility = ''
  2898. const item = ancestor(r.target, '.item.nicoadVideoItem')
  2899. callback([SearchPage._parseAdItem(item)])
  2900. return
  2901. }
  2902. }
  2903. }).observe(node, {
  2904. attributes: true,
  2905. attributeOldValue: true,
  2906. attributeFilter: ['style'],
  2907. })
  2908. }
  2909. },
  2910. get _pendingMoviesInvisibleCss() {
  2911. return `.contentBody.video.uad .item,
  2912. #tsukuaso .item,
  2913. .contentBody.video.uad .nicoadVideoItemWrapper {
  2914. visibility: hidden;
  2915. }
  2916. .contentBody.video.uad .item[data-video-item-muted],
  2917. .contentBody.video.uad .item[data-video-item-sensitive],
  2918. .contentBody.video.uad .item.nrn-thumb-info-done,
  2919. #tsukuaso .item.nrn-thumb-info-done,
  2920. .contentBody.video.uad.searchUad .item,
  2921. .contentBody.video.uad .nicoadVideoItemWrapper.nrn-thumb-info-done,
  2922. .contentBody.video.uad .nicoadVideoItemWrapper.nrn-thumb-info-done .item {
  2923. visibility: inherit;
  2924. }
  2925. `
  2926. },
  2927. get css() {
  2928. return `#nrn-config-bar {
  2929. margin: 10px 0;
  2930. }
  2931. #nrn-config-button,
  2932. .nrn-visit-button:hover,
  2933. .nrn-movie-ng-button:hover,
  2934. .nrn-title-ng-button:hover,
  2935. .nrn-tag-ng-button:hover,
  2936. .nrn-contributor-ng-button:hover,
  2937. .nrn-contributor-ng-id-button:hover,
  2938. .nrn-contributor-ng-name-button:hover,
  2939. .nrn-movie-info-toggle:hover,
  2940. .nrn-description-open-button:hover,
  2941. .nrn-description-close-button:hover {
  2942. text-decoration: underline;
  2943. cursor: pointer;
  2944. }
  2945. .nrn-description-open-button {
  2946. position: absolute;
  2947. bottom: 0;
  2948. right: 0;
  2949. background-color: white;
  2950. }
  2951. .nrn-description-text,
  2952. .nrn-description-close-button {
  2953. display: block;
  2954. }
  2955. .nrn-description-close-button {
  2956. text-align: right;
  2957. }
  2958. .itemData,
  2959. .itemDescription,
  2960. .nicoadVideoItemWrapper {
  2961. position: relative;
  2962. }
  2963. .nrn-movie-tag {
  2964. display: inline-block;
  2965. margin-right: 1em;
  2966. }
  2967. .nrn-description-open-button,
  2968. .nrn-description-close-button,
  2969. .nrn-movie-tag-link,
  2970. .nrn-contributor-link {
  2971. color: #333333;
  2972. }
  2973. .nrn-movie-tag-link.nrn-movie-ng-tag-link,
  2974. .nrn-contributor-link.nrn-ng-contributor-link,
  2975. .nrn-matched-ng-contributor-name,
  2976. .nrn-matched-ng-title {
  2977. color: white;
  2978. background-color: fuchsia;
  2979. }
  2980. .nrn-movie-info-container .nrn-action-pane {
  2981. line-height: 1.3em;
  2982. padding-top: 4px;
  2983. }
  2984. .nrn-movie-info-container .nrn-tag-container,
  2985. .nrn-movie-info-container .nrn-contributor-container {
  2986. line-height: 1.5em;
  2987. padding-top: 4px;
  2988. }
  2989. .videoList01 .itemContent .itemDescription.ranking.nrn-description {
  2990. height: auto;
  2991. width: auto;
  2992. }
  2993. .nrn-movie-info-toggle {
  2994. color: #333333;
  2995. font-size: 85%;
  2996. }
  2997. .videoList01 .nrn-movie-info-toggle {
  2998. position: absolute;
  2999. right: 0;
  3000. top: 0;
  3001. }
  3002. .videoList02 .nrn-movie-info-toggle,
  3003. .nrn-4-column-item .nrn-movie-info-toggle {
  3004. display: block;
  3005. text-align: right;
  3006. }
  3007. .videoList02 .nrn-movie-info-container {
  3008. clear: both;
  3009. }
  3010. .nrn-hide,
  3011. .videoList02 .item.nrn-hide,
  3012. .video .item.nrn-4-column-item.nrn-hide,
  3013. .uad .nicoadVideoItemWrapper .nicoadVideoItem.nrn-hide,
  3014. .item[data-video-item-muted] {
  3015. display: none;
  3016. }
  3017. .item.nrn-reduce .videoList01Wrap,
  3018. .item.nrn-reduce .videoList02Wrap {
  3019. width: 80px;
  3020. }
  3021. .item.nrn-reduce .itemThumbBox,
  3022. .item.nrn-reduce .itemThumbBox .itemThumb,
  3023. .item.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap,
  3024. .item.nrn-reduce .itemThumbBox .itemThumb .itemThumbWrap img,
  3025. .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox,
  3026. .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb,
  3027. .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb .itemThumbWrap,
  3028. .nicoadVideoItemWrapper.nrn-reduce .item .itemThumbBox .itemThumb .itemThumbWrap img {
  3029. width: 80px;
  3030. height: 45px;
  3031. }
  3032. .videoList01 .nrn-action-pane,
  3033. .videoList02 .nrn-action-pane {
  3034. display: none;
  3035. position: absolute;
  3036. top: 0px;
  3037. right: 0px;
  3038. padding: 3px;
  3039. color: #999;
  3040. background-color: rgb(105, 105, 105);
  3041. z-index: 11;
  3042. }
  3043. .videoList02 .nrn-action-pane {
  3044. font-size: 85%;
  3045. }
  3046. .videoList01 .item:hover .nrn-action-pane,
  3047. .videoList02 .item:hover .nrn-action-pane,
  3048. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane,
  3049. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane {
  3050. display: block;
  3051. }
  3052. .videoList01 .item:hover .nrn-action-pane .nrn-visit-button,
  3053. .videoList01 .item:hover .nrn-action-pane .nrn-movie-ng-button,
  3054. .videoList01 .item:hover .nrn-action-pane .nrn-title-ng-button,
  3055. .videoList02 .item:hover .nrn-action-pane .nrn-visit-button,
  3056. .videoList02 .item:hover .nrn-action-pane .nrn-movie-ng-button,
  3057. .videoList02 .item:hover .nrn-action-pane .nrn-title-ng-button,
  3058. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-visit-button,
  3059. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button,
  3060. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button,
  3061. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-visit-button,
  3062. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button,
  3063. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button {
  3064. color: white;
  3065. }
  3066. .videoList01 .item:hover .nrn-action-pane .nrn-movie-ng-button,
  3067. .videoList01 .item:hover .nrn-action-pane .nrn-title-ng-button,
  3068. .videoList02 .item:hover .nrn-action-pane .nrn-movie-ng-button,
  3069. .videoList02 .item:hover .nrn-action-pane .nrn-title-ng-button,
  3070. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button,
  3071. .videoList01 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button,
  3072. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-movie-ng-button,
  3073. .videoList02 .nicoadVideoItemWrapper:hover .nrn-action-pane .nrn-title-ng-button {
  3074. margin-left: 5px;
  3075. border-left: solid thin;
  3076. padding-left: 5px;
  3077. }
  3078. .nrn-user-ng-button,
  3079. .nrn-tag-ng-button {
  3080. display: inline-block;
  3081. }
  3082. .nrn-ng-movie-title,
  3083. .nrn-contributor-link.nrn-ng-id-contributor-link {
  3084. text-decoration: line-through;
  3085. }
  3086. .nrn-sub-movie-root {
  3087. position: relative;
  3088. }
  3089. .nrn-sub-movie-root .nrn-movie-info-toggle {
  3090. display: block;
  3091. text-align: right;
  3092. background-color: white;
  3093. }
  3094. .nrn-sub-movie-root .nrn-movie-info-container {
  3095. clear: left;
  3096. padding: 10px 0 15px 0;
  3097. }
  3098. .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button,
  3099. .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button,
  3100. .nrn-sub-movie-root .nrn-action-pane .nrn-title-ng-button,
  3101. .nrn-4-column-item .nrn-action-pane .nrn-visit-button,
  3102. .nrn-4-column-item .nrn-action-pane .nrn-movie-ng-button,
  3103. .nrn-4-column-item .nrn-action-pane .nrn-title-ng-button {
  3104. display: inline-block;
  3105. color: #333333;
  3106. }
  3107. .nrn-sub-movie-root .nrn-action-pane .nrn-visit-button,
  3108. .nrn-sub-movie-root .nrn-action-pane .nrn-movie-ng-button,
  3109. .nrn-4-column-item .nrn-action-pane .nrn-visit-button,
  3110. .nrn-4-column-item .nrn-action-pane .nrn-movie-ng-button {
  3111. margin-right: 0.5em;
  3112. }
  3113. .nrn-error {
  3114. color: red;
  3115. }
  3116. .videoList02 .item,
  3117. .video .item.nrn-4-column-item {
  3118. float: none;
  3119. display: inline-block;
  3120. vertical-align: top;
  3121. }
  3122. .video .item.nrn-4-column-item:nth-child(4n+1) {
  3123. clear: none;
  3124. }
  3125. .nrn-4-column-item .nrn-movie-tag {
  3126. display: block;
  3127. }
  3128. `
  3129. },
  3130. })
  3131. Object.assign(SearchPage, {
  3132. TwoColumnMovieRoot,
  3133. FourColumnMovieRoot,
  3134. is(location) {
  3135. var p = location.pathname
  3136. return p.startsWith('/search/') || p.startsWith('/tag/')
  3137. },
  3138. _isGettingAdDone(mutationRecord) {
  3139. const r = mutationRecord
  3140. return r.attributeName === 'style'
  3141. && r.oldValue.includes('visibility: hidden;')
  3142. && r.target.getAttribute('style').includes('visibility: visible;')
  3143. },
  3144. _parseAdItem(item) {
  3145. const p = item.querySelector('.count.ads .value a').pathname
  3146. return {
  3147. type: 'ad',
  3148. movie: {
  3149. id: p.slice(p.lastIndexOf('/') + 1),
  3150. title: item.querySelector('.itemTitle a').textContent,
  3151. },
  3152. rootElem: ancestor(item, '.nicoadVideoItemWrapper'),
  3153. }
  3154. },
  3155. })
  3156. return SearchPage
  3157. })(NicoPage)
  3158.  
  3159. var Controller = (function() {
  3160. var isMovieAnchor = function(e) {
  3161. return e.dataset.nrnMovieAnchor === 'true'
  3162. }
  3163. var movieAnchor = function(child) {
  3164. for (var n = child; n; n = n.parentNode) {
  3165. if (n.nodeType !== Node.ELEMENT_NODE) return null
  3166. if (isMovieAnchor(n)) return n
  3167. }
  3168. }
  3169. var dataOfMovieAnchor = function(e) {
  3170. return {
  3171. id: e.dataset.nrnMovieId,
  3172. title: e.dataset.nrnMovieTitle,
  3173. }
  3174. }
  3175. var Controller = function(config, page) {
  3176. this.config = config
  3177. this.page = page
  3178. }
  3179. Controller.prototype = {
  3180. addListenersTo(eventTarget) {
  3181. eventTarget.addEventListener('change', this._changed.bind(this))
  3182. eventTarget.addEventListener('click', this._clicked.bind(this))
  3183. },
  3184. _changed(event) {
  3185. switch (event.target.id) {
  3186. case 'nrn-visited-movie-view-mode-select':
  3187. this.config.visitedMovieViewMode.value = event.target.value; break
  3188. case 'nrn-visible-contributor-type-select':
  3189. this.config.visibleContributorType.value = event.target.value; break
  3190. case 'nrn-ng-movie-visible-checkbox':
  3191. this.config.ngMovieVisible.value = event.target.checked; break
  3192. }
  3193. },
  3194. _addVisitedMovie(target) {
  3195. var d = dataOfMovieAnchor(movieAnchor(target))
  3196. this.config.visitedMovies.addAsync(d.id, d.title)
  3197. },
  3198. _toggleData(target, add, remove) {
  3199. var ds = target.dataset
  3200. switch (ds.type) {
  3201. case 'add': add.call(this, ds); break
  3202. case 'remove': remove.call(this, ds); break
  3203. default: throw new Error(ds.type)
  3204. }
  3205. },
  3206. _toggleVisitedMovie(target) {
  3207. this._toggleData(target, function(ds) {
  3208. this.config.visitedMovies.addAsync(ds.movieId, ds.movieTitle)
  3209. }, function(ds) {
  3210. this.config.visitedMovies.removeAsync([ds.movieId])
  3211. })
  3212. },
  3213. _toggleNgMovie(target) {
  3214. this._toggleData(target, function(ds) {
  3215. this.config.ngMovies.addAsync(ds.movieId, ds.movieTitle)
  3216. }, function(ds) {
  3217. this.config.ngMovies.removeAsync([ds.movieId])
  3218. })
  3219. },
  3220. _toggleNgTitle(target) {
  3221. this._toggleData(target, function(ds) {
  3222. ConfigDialog.promptNgTitle(this.config, ds.movieTitle)
  3223. }, function(ds) {
  3224. this.config.ngTitles.removeAsync([ds.ngTitle])
  3225. })
  3226. },
  3227. _toggleNgTag(target) {
  3228. this._toggleData(target, function(ds) {
  3229. if (this.config.addToNgLockedTags.value && ds.lock) {
  3230. this.config.ngLockedTags.addAsync(ds.tagName);
  3231. } else {
  3232. this.config.ngTags.addAsync(ds.tagName);
  3233. }
  3234. }, function(ds) {
  3235. this.config.ngTags.removeAsync([ds.tagName])
  3236. this.config.ngLockedTags.removeAsync([ds.tagName])
  3237. })
  3238. },
  3239. _toggleContributorNgId(target) {
  3240. var name = function(ds) {
  3241. return Contributor.new(ds.contributorType, ds.id, ds.name).ngIdStoreName
  3242. }
  3243. this._toggleData(target, function(ds) {
  3244. this.config[name(ds)].addAsync(parseInt(ds.id), ds.name)
  3245. }, function(ds) {
  3246. this.config[name(ds)].removeAsync([parseInt(ds.id)])
  3247. })
  3248. },
  3249. _toggleNgUserName(target) {
  3250. this._toggleData(target, function(ds) {
  3251. ConfigDialog.promptNgUserName(this.config, ds.name)
  3252. }, function(ds) {
  3253. this.config.ngUserNames.removeAsync([ds.matched])
  3254. })
  3255. },
  3256. _clicked(event) {
  3257. var e = event.target
  3258. if (e.id === 'nrn-config-button') {
  3259. this.page.showConfigDialog(this.config)
  3260. } else if (movieAnchor(e)) {
  3261. this._addVisitedMovie(e)
  3262. } else if (e.classList.contains('nrn-visit-button')) {
  3263. this._toggleVisitedMovie(e)
  3264. } else if (e.classList.contains('nrn-movie-ng-button')) {
  3265. this._toggleNgMovie(e)
  3266. } else if (e.classList.contains('nrn-title-ng-button')) {
  3267. this._toggleNgTitle(e)
  3268. } else if (e.classList.contains('nrn-movie-info-toggle')) {
  3269. this.page.getMovieRootBy(e).toggleMovieInfo()
  3270. } else if (e.classList.contains('nrn-description-open-button')
  3271. || e.classList.contains('nrn-description-close-button')) {
  3272. this.page.getMovieRootBy(e).toggleDescription()
  3273. } else if (e.classList.contains('nrn-tag-ng-button')) {
  3274. this._toggleNgTag(e)
  3275. } else if (e.classList.contains('nrn-contributor-ng-button')) {
  3276. this._toggleContributorNgId(e)
  3277. } else if (e.classList.contains('nrn-contributor-ng-id-button')) {
  3278. this._toggleContributorNgId(e)
  3279. } else if (e.classList.contains('nrn-contributor-ng-name-button')) {
  3280. this._toggleNgUserName(e)
  3281. }
  3282. },
  3283. }
  3284. return Controller
  3285. })()
  3286.  
  3287. var Main = (function() {
  3288. var createMovieRoot = function(resultOfParsing, page, movieViewMode) {
  3289. var movie = movieViewMode.movie
  3290. var result = page.createMovieRoot(resultOfParsing)
  3291. result.actionPane
  3292. = new NicoPage.ActionPane(page.doc, movie).bindToMovie(movie)
  3293. result.setMovieInfoToggleIfRequired()
  3294. result.markMovieAnchor()
  3295. result.id = movie.id
  3296. result.title = movie.title
  3297. result.bindToMovieViewMode(movieViewMode)
  3298. result.bindToConfig(movieViewMode.config)
  3299. result.bindToMovie(movie)
  3300. return result
  3301. }
  3302. var createMovieRoots = function(resultsOfParsing, model, page) {
  3303. for (var r of resultsOfParsing) {
  3304. var movie = model.movies.get(r.movie.id)
  3305. var movieViewMode = model.movieViewModes.get(movie)
  3306. var root = createMovieRoot(r, page, movieViewMode)
  3307. root.movieTitle = new NicoPage.MovieTitle(root.titleElem).bindToMovie(movie)
  3308. page.mapToggleTo(root)
  3309. }
  3310. }
  3311. var setup = function(resultsOfParsing, model, page) {
  3312. model.createMovies(resultsOfParsing)
  3313. createMovieRoots(resultsOfParsing, model, page)
  3314. }
  3315. var createMessageElem = function(doc, message) {
  3316. var result = doc.createElement('p')
  3317. result.textContent = message
  3318. return result
  3319. }
  3320. function gmXmlHttpRequest() {
  3321. if (typeof GM_xmlhttpRequest === 'undefined')
  3322. return GM.xmlHttpRequest
  3323. return GM_xmlhttpRequest
  3324. }
  3325. var createThumbInfoRequester = function(movies, movieViewModes) {
  3326. var thumbInfo = new ThumbInfo(gmXmlHttpRequest())
  3327. .on('completed', ThumbInfoListener.forCompleted(movies))
  3328. .on('errorOccurred', ThumbInfoListener.forErrorOccurred(movies))
  3329. return function(prefer) {
  3330. thumbInfo.request(
  3331. movieViewModes.sort().map(function(m) { return m.movie.id }), prefer)
  3332. }
  3333. }
  3334. var getThumbInfoRequester = function(movies, movieViewModes) {
  3335. return movies.config.useGetThumbInfo.value
  3336. ? createThumbInfoRequester(movies, movieViewModes)
  3337. : function() {}
  3338. }
  3339. var createModel = function(config) {
  3340. var movies = new Movies(config)
  3341. var movieViewModes = new MovieViewModes(config)
  3342. var requestThumbInfo = getThumbInfoRequester(movies, movieViewModes)
  3343. return {
  3344. config,
  3345. movies,
  3346. movieViewModes,
  3347. requestThumbInfo,
  3348. createMovies(resultsOfParsing) {
  3349. movies.setIfAbsent(resultsOfParsing.map(function(r) {
  3350. return new Movie(r.movie.id, r.movie.title)
  3351. }))
  3352. },
  3353. }
  3354. }
  3355. var createView = function(page) {
  3356. var configBar = page.createConfigBar()
  3357. return {
  3358. page,
  3359. addConfigBar() {
  3360. page.addConfigBar(configBar)
  3361. },
  3362. _bindToConfig(config) {
  3363. page.bindToConfig(config)
  3364. configBar.bindToConfig(config)
  3365. },
  3366. bindToModel(model) {
  3367. this._bindToConfig(model.config)
  3368. },
  3369. bindToWindow() {
  3370. },
  3371. setup(model, targetElem) {
  3372. setup(page.parse(targetElem), model, page)
  3373. },
  3374. setupAndRequestThumbInfo(model, targetElem) {
  3375. this.setup(model, targetElem)
  3376. model.requestThumbInfo()
  3377. },
  3378. observeMutation(model) {
  3379. page.observeMutation(function(resultOfParsing, prefer) {
  3380. setup(resultOfParsing, model, page)
  3381. model.requestThumbInfo(prefer)
  3382. })
  3383. },
  3384. }
  3385. }
  3386. function addStyle(style) {
  3387. const e = document.createElement('style');
  3388. e.textContent = style;
  3389. document.head.appendChild(e);
  3390. }
  3391. function gmGetValue() {
  3392. if (typeof GM_getValue === 'undefined')
  3393. return GM.getValue
  3394. return GM_getValue
  3395. }
  3396. function gmSetValue() {
  3397. if (typeof GM_setValue === 'undefined')
  3398. return GM.setValue
  3399. return GM_setValue
  3400. }
  3401. function handleAutoPagerizedNodes(model, page) {
  3402. return function(e) {
  3403. const t = e.target
  3404. if (t.nodeType === Node.ELEMENT_NODE && t.classList.contains('item')) {
  3405. setup(page.parseAutoPagerizedNodes(t), model, page)
  3406. model.requestThumbInfo()
  3407. }
  3408. }
  3409. }
  3410. var domContentLoaded = function(page) {
  3411. return async function() {
  3412. try {
  3413. addStyle(page.css)
  3414. const config = new Config(gmGetValue(), gmSetValue())
  3415. await config.sync()
  3416. var model = createModel(config)
  3417. var view = createView(page)
  3418. view.addConfigBar()
  3419. view.bindToModel(model)
  3420. view.bindToWindow()
  3421. view.setupAndRequestThumbInfo(model)
  3422. view.observeMutation(model)
  3423. new Controller(model.config, page).addListenersTo(page.doc.body)
  3424. if (!model.config.useGetThumbInfo.value) {
  3425. page.pendingMoviesVisible = true
  3426. }
  3427. page.doc.body.addEventListener('AutoPagerize_DOMNodeInserted',
  3428. handleAutoPagerizedNodes(model, page))
  3429. } catch (e) {
  3430. console.error(e)
  3431. page.pendingMoviesVisible = true
  3432. }
  3433. }
  3434. }
  3435. var getPage = function() {
  3436. if (SearchPage.is(document.location)) return new SearchPage(document)
  3437. return new ListPage(document)
  3438. }
  3439. var main = function() {
  3440. var page = getPage()
  3441. page.pendingMoviesVisible = false
  3442. if (['interactive', 'complete'].includes(document.readyState))
  3443. domContentLoaded(page)()
  3444. else
  3445. page.doc.addEventListener('DOMContentLoaded', domContentLoaded(page))
  3446. }
  3447. return {main}
  3448. })()
  3449.  
  3450. Main.main()
  3451. })()