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