- // ==UserScript==
- // @name Mylist Filter
- // @namespace https://github.com/segabito/
- // @description 視聴不可能な動画だけ表示して一括削除とかできるやつ
- // @match *://www.nicovideo.jp/my/mylist*
- // @grant none
- // @author 名無しさん@匿名希望
- // @version 0.0.1
- // @run-at document-body
- // @license public domain
- // @noframes
- // ==/UserScript==
- /* eslint-disable */
-
-
- (async (window) => {
- const global = {
- PRODUCT: 'MylistFilter'
- };
- function EmitterInitFunc() {
- class Handler { //extends Array {
- constructor(...args) {
- this._list = args;
- }
- get length() {
- return this._list.length;
- }
- exec(...args) {
- if (!this._list.length) {
- return;
- } else if (this._list.length === 1) {
- this._list[0](...args);
- return;
- }
- for (let i = this._list.length - 1; i >= 0; i--) {
- this._list[i](...args);
- }
- }
- execMethod(name, ...args) {
- if (!this._list.length) {
- return;
- } else if (this._list.length === 1) {
- this._list[0][name](...args);
- return;
- }
- for (let i = this._list.length - 1; i >= 0; i--) {
- this._list[i][name](...args);
- }
- }
- add(member) {
- if (this._list.includes(member)) {
- return this;
- }
- this._list.unshift(member);
- return this;
- }
- remove(member) {
- this._list = this._list.filter(m => m !== member);
- return this;
- }
- clear() {
- this._list.length = 0;
- return this;
- }
- get isEmpty() {
- return this._list.length < 1;
- }
- *[Symbol.iterator]() {
- const list = this._list || [];
- for (const member of list) {
- yield member;
- }
- }
- next() {
- return this[Symbol.iterator]();
- }
- }
- Handler.nop = () => {/* ( ˘ω˘ ) スヤァ */};
- const PromiseHandler = (() => {
- const id = function() { return `Promise${this.id++}`; }.bind({id: 0});
- class PromiseHandler extends Promise {
- constructor(callback = () => {}) {
- const key = new Object({id: id(), callback, status: 'pending'});
- const cb = function(res, rej) {
- const resolve = (...args) => { this.status = 'resolved'; this.value = args; res(...args); };
- const reject = (...args) => { this.status = 'rejected'; this.value = args; rej(...args); };
- if (this.result) {
- return this.result.then(resolve, reject);
- }
- Object.assign(this, {resolve, reject});
- return callback(resolve, reject);
- }.bind(key);
- super(cb);
- this.resolve = this.resolve.bind(this);
- this.reject = this.reject.bind(this);
- this.key = key;
- }
- resolve(...args) {
- if (this.key.resolve) {
- this.key.resolve(...args);
- } else {
- this.key.result = Promise.resolve(...args);
- }
- return this;
- }
- reject(...args) {
- if (this.key.reject) {
- this.key.reject(...args);
- } else {
- this.key.result = Promise.reject(...args);
- }
- return this;
- }
- addCallback(callback) {
- Promise.resolve().then(() => callback(this.resolve, this.reject));
- return this;
- }
- }
- return PromiseHandler;
- })();
- const {Emitter} = (() => {
- let totalCount = 0;
- let warnings = [];
- class Emitter {
- on(name, callback) {
- if (!this._events) {
- Emitter.totalCount++;
- this._events = new Map();
- }
- name = name.toLowerCase();
- let e = this._events.get(name);
- if (!e) {
- e = this._events.set(name, new Handler(callback));
- } else {
- e.add(callback);
- }
- if (e.length > 10) {
- !Emitter.warnings.includes(this) && Emitter.warnings.push(this);
- }
- return this;
- }
- off(name, callback) {
- if (!this._events) {
- return;
- }
- name = name.toLowerCase();
- const e = this._events.get(name);
- if (!this._events.has(name)) {
- return;
- } else if (!callback) {
- this._events.delete(name);
- } else {
- e.remove(callback);
- if (e.isEmpty) {
- this._events.delete(name);
- }
- }
- if (this._events.size < 1) {
- delete this._events;
- }
- return this;
- }
- once(name, func) {
- const wrapper = (...args) => {
- func(...args);
- this.off(name, wrapper);
- wrapper._original = null;
- };
- wrapper._original = func;
- return this.on(name, wrapper);
- }
- clear(name) {
- if (!this._events) {
- return;
- }
- if (name) {
- this._events.delete(name);
- } else {
- delete this._events;
- Emitter.totalCount--;
- }
- return this;
- }
- emit(name, ...args) {
- if (!this._events) {
- return;
- }
- name = name.toLowerCase();
- const e = this._events.get(name);
- if (!e) {
- return;
- }
- e.exec(...args);
- return this;
- }
- emitAsync(...args) {
- if (!this._events) {
- return;
- }
- setTimeout(() => this.emit(...args), 0);
- return this;
- }
- promise(name, callback) {
- if (!this._promise) {
- this._promise = new Map;
- }
- const p = this._promise.get(name);
- if (p) {
- return callback ? p.addCallback(callback) : p;
- }
- this._promise.set(name, new PromiseHandler(callback));
- return this._promise.get(name);
- }
- emitResolve(name, ...args) {
- if (!this._promise) {
- this._promise = new Map;
- }
- if (!this._promise.has(name)) {
- this._promise.set(name, new PromiseHandler());
- }
- return this._promise.get(name).resolve(...args);
- }
- emitReject(name, ...args) {
- if (!this._promise) {
- this._promise = new Map;
- }
- if (!this._promise.has(name)) {
- this._promise.set(name, new PromiseHandler);
- }
- return this._promise.get(name).reject(...args);
- }
- resetPromise(name) {
- if (!this._promise) { return; }
- this._promise.delete(name);
- }
- hasPromise(name) {
- return this._promise && this._promise.has(name);
- }
- addEventListener(...args) { return this.on(...args); }
- removeEventListener(...args) { return this.off(...args);}
- }
- Emitter.totalCount = totalCount;
- Emitter.warnings = warnings;
- return {Emitter};
- })();
- return {Handler, PromiseHandler, Emitter};
- }
- const {Handler, PromiseHandler, Emitter} = EmitterInitFunc();
- const dimport = (() => {
- try { // google先生の真似
- return new Function('u', 'return import(u)');
- } catch(e) {
- const map = {};
- let count = 0;
- return url => {
- if (map[url]) {
- return map[url];
- }
- try {
- const now = Date.now();
- const callbackName = `dimport_${now}_${count++}`;
- const loader = `
- import * as module${now} from "${url}";
- console.log('%cdynamic import from "${url}"',
- 'font-weight: bold; background: #333; color: #ff9; display: block; padding: 4px; width: 100%;');
- window.${callbackName}(module${now});
- `.trim();
- window.console.time(`"${url}" import time`);
- const p = new Promise((ok, ng) => {
- const s = document.createElement('script');
- s.type = 'module';
- s.onerror = ng;
- s.append(loader);
- s.dataset.import = url;
- window[callbackName] = module => {
- window.console.timeEnd(`"${url}" import time`);
- ok(module);
- delete window[callbackName];
- };
- document.head.append(s);
- });
- map[url] = p;
- return p;
- } catch (e) {
- console.warn(url, e);
- return Promise.reject(e);
- }
- };
- }
- })();
- const bounce = {
- origin: Symbol('origin'),
- raf(func) {
- let reqId = null;
- let lastArgs = null;
- const callback = () => {
- const lastResult = func(...lastArgs);
- reqId = lastArgs = null;
- };
- const result = (...args) => {
- if (reqId) {
- cancelAnimationFrame(reqId);
- }
- lastArgs = args;
- reqId = requestAnimationFrame(callback);
- };
- result[this.origin] = func;
- return result;
- },
- idle(func, time) {
- let reqId = null;
- let lastArgs = null;
- let promise = new PromiseHandler();
- const [caller, canceller] =
- (time === undefined && window.requestIdleCallback) ?
- [window.requestIdleCallback, window.cancelIdleCallback] : [window.setTimeout, window.clearTimeout];
- const callback = () => {
- const lastResult = func(...lastArgs);
- promise.resolve({lastResult, lastArgs});
- reqId = lastArgs = null;
- promise = new PromiseHandler();
- };
- const result = (...args) => {
- if (reqId) {
- reqId = canceller(reqId);
- }
- lastArgs = args;
- reqId = caller(callback, time);
- return promise;
- };
- result[this.origin] = func;
- return result;
- },
- time(func, time = 0) {
- return this.idle(func, time);
- }
- };
- const throttle = (func, interval) => {
- let lastTime = 0;
- let timer;
- let promise = new PromiseHandler();
- const result = (...args) => {
- const now = performance.now();
- const timeDiff = now - lastTime;
- if (timeDiff < interval) {
- if (!timer) {
- timer = setTimeout(() => {
- lastTime = performance.now();
- timer = null;
- const lastResult = func(...args);
- promise.resolve({lastResult, lastArgs: args});
- promise = new PromiseHandler();
- }, Math.max(interval - timeDiff, 0));
- }
- return;
- }
- if (timer) {
- timer = clearTimeout(timer);
- }
- lastTime = now;
- const lastResult = func(...args);
- promise.resolve({lastResult, lastArgs: args});
- promise = new PromiseHandler();
- };
- result.cancel = () => {
- if (timer) {
- timer = clearTimeout(timer);
- }
- promise.resolve({lastResult: null, lastArgs: null});
- promise = new PromiseHandler();
- };
- return result;
- };
- throttle.raf = func => {
- let raf;
- const result = (...args) => {
- if (raf) {
- return;
- }
- raf = requestAnimationFrame(() => {
- raf = null;
- func(...args);
- });
- };
- result.cancel = () => {
- if (raf) {
- raf = cancelAnimationFrame(raf);
- }
- };
- return result;
- };
- throttle.idle = func => {
- let id;
- const request = (self.requestIdleCallback || self.setTimeout);
- const cancel = (self.cancelIdleCallback || self.clearTimeout);
- const result = (...args) => {
- if (id) {
- return;
- }
- id = request(() => {
- id = null;
- func(...args);
- }, 0);
- };
- result.cancel = () => {
- if (id) {
- id = cancel(id);
- }
- };
- return result;
- };
- const css = (() => {
- const setPropsTask = [];
- const applySetProps = throttle.raf(() => {
- const tasks = setPropsTask.concat();
- setPropsTask.length = 0;
- for (const [element, prop, value] of tasks) {
- try {
- element.style.setProperty(prop, value);
- } catch (error) {
- console.warn('element.style.setProperty fail', {element, prop, value, error});
- }
- }
- });
- const css = {
- addStyle: (styles, option, document = window.document) => {
- const elm = Object.assign(document.createElement('style'), {
- type: 'text/css'
- }, typeof option === 'string' ? {id: option} : (option || {}));
- if (typeof option === 'string') {
- elm.id = option;
- } else if (option) {
- Object.assign(elm, option);
- }
- elm.classList.add(global.PRODUCT);
- elm.append(styles.toString());
- (document.head || document.body || document.documentElement).append(elm);
- elm.disabled = option && option.disabled;
- elm.dataset.switch = elm.disabled ? 'off' : 'on';
- return elm;
- },
- registerProps(...args) {
- if (!CSS || !('registerProperty' in CSS)) {
- return;
- }
- for (const definition of args) {
- try {
- (definition.window || window).CSS.registerProperty(definition);
- } catch (err) { console.warn('CSS.registerProperty fail', definition, err); }
- }
- },
- setProps(...tasks) {
- setPropsTask.push(...tasks);
- if (setPropsTask.length) {
- applySetProps();
- }
- return Promise.resolve();
- },
- addModule: async function(func, options = {}) {
- if (!CSS || !('paintWorklet' in CSS) || this.set.has(func)) {
- return;
- }
- this.set.add(func);
- const src =
- `(${func.toString()})(
- this,
- registerPaint,
- ${JSON.stringify(options.config || {}, null, 2)}
- );`;
- const blob = new Blob([src], {type: 'text/javascript'});
- const url = URL.createObjectURL(blob);
- await CSS.paintWorklet.addModule(url).then(() => URL.revokeObjectURL(url));
- return true;
- }.bind({set: new WeakSet}),
- escape: value => CSS.escape ? CSS.escape(value) : value.replace(/([\.#()[\]])/g, '\\$1'),
- number: value => CSS.number ? CSS.number(value) : value,
- s: value => CSS.s ? CSS.s(value) : `${value}s`,
- ms: value => CSS.ms ? CSS.ms(value) : `${value}ms`,
- pt: value => CSS.pt ? CSS.pt(value) : `${value}pt`,
- px: value => CSS.px ? CSS.px(value) : `${value}px`,
- percent: value => CSS.percent ? CSS.percent(value) : `${value}%`,
- vh: value => CSS.vh ? CSS.vh(value) : `${value}vh`,
- vw: value => CSS.vw ? CSS.vw(value) : `${value}vw`,
- trans: value => self.CSSStyleValue ? CSSStyleValue.parse('transform', value) : value,
- word: value => self.CSSKeywordValue ? new CSSKeywordValue(value) : value,
- image: value => self.CSSStyleValue ? CSSStyleValue.parse('background-image', value) : value,
- };
- return css;
- })();
- const cssUtil = css;
- const [lit] = await Promise.all([
- dimport('https://unpkg.com/lit-html?module')
- ]);
- const {html} = lit;
- const $ = self.jQuery;
-
- cssUtil.addStyle(`
- .ItemSelectMenuContainer-itemSelect {
- display: grid;
- grid-template-columns: 160px 1fr
- }
-
- .itemFilterContainer {
- display: grid;
- background: #f0f0f0;
- grid-template-rows: 1fr 1fr;
- grid-template-columns: auto 1fr;
- user-select: none;
- }
-
- .itemFilterContainer-title {
- grid-row: 1 / 3;
- grid-column: 1 / 2;
- display: flex;
- align-items: center;
- white-space: nowrap;
- padding: 8px;
- }
-
- .playableFilter {
- grid-row: 1;
- grid-column: 2;
- padding: 4px 8px;
- }
-
- .wordFilter {
- grid-row: 2;
- grid-column: 2;
- padding: 0 8px 4px;
- }
-
- .playableFilter, .wordFilter {
- display: inline-flex;
- align-items: center;
- }
-
- .playableFilter .caption, .wordFilter .caption {
- display: inline-block;
- margin-right: 8px;
- }
-
- .playableFilter input[type="radio"] {
- transform: scale(1.2);
- margin-right: 8px;
- }
-
- .playableFilter label {
- display: inline-flex;
- align-items: center;
- padding: 0 8px;
- }
-
- .playableFilter input[checked] + span {
- background: linear-gradient(transparent 80%, #99ccff 0%);
- }
-
- .wordFilter input[type="text"] {
- padding: 4px;
- }
- .wordFilter input[type="button"] {
- padding: 4px;
- border: 1px solid #ccc;
- }
- .wordFilter input[type="button"]:hover::before {
- content: '・';
- }
- .wordFilter input[type="button"]:hover::after {
- content: '・';
- }
- `);
-
- const playableFilterTpl = props => {
- const playable = props.playable || '';
- return html`
- <div class="playableFilter">
- <span class="caption">状態</span>
- <label
- data-click-command="set-playable-filter"
- data-command-param=""
- >
- <input type="radio" name="playable-filter" value=""
- ?checked=${playable !== 'playable' && playable !== 'not-playable'}>
- <span>指定なし</span>
- </label>
- <label
- data-click-command="set-playable-filter"
- data-command-param="playable"
- >
- <input type="radio" name="playable-filter" value="playable"
- ?checked=${playable === 'playable'}>
- <span>視聴可能</span>
- </label>
- <label
- data-click-command="set-playable-filter"
- data-command-param="not-playable"
- >
- <input type="radio" name="playable-filter" value="not-playable"
- ?checked=${playable === 'not-playable'}>
- <span>視聴不可</span>
- </label>
- </div>`;
- };
-
- const wordFilterTpl = props => {
- return html`
- <div class="wordFilter">
- <input type="text" name="word-filter" class="wordFilterInput" placeholder="キーワード"
- value=${props.word || ''}>
- <input type="button" data-click-command="clear-word-filter"
- title="・✗・" value=" ✗ ">
- <small> タイトル・マイリストコメント検索</small>
- </div>`;
- };
-
- const resetForm = () => {
- [...document.querySelectorAll('.itemFilterContainer input[name="playable-filter"]')]
- .forEach(r => r.checked = r.hasAttribute('checked'));
- [...document.querySelectorAll('.wordFilterInput')]
- .forEach(r => r.value = r.getAttribute('value'));
- };
-
- const itemFilterContainer = Object.assign(document.createElement('div'), {
- className: 'itemFilterContainer'
- });
-
- const render = props => {
- if (!document.body.contains(itemFilterContainer)) {
- const parentNode = document.querySelector('.ItemSelectMenuContainer-itemSelect');
- if (parentNode) {
- parentNode.append(itemFilterContainer);
- }
- }
-
- lit.render(html`
- <div class="itemFilterContainer-title">絞り込み</div>
- ${playableFilterTpl(props)}
- ${wordFilterTpl(props)}
- `, itemFilterContainer);
-
- resetForm();
- };
-
- let override = false;
- const overrideFilter = () => {
- if (!window.MylistHelper || override) {
- return;
- }
- override = true;
- const self = window.MylistHelper.itemFilter;
- Object.defineProperty(self, 'wordFilterCallback', {
- get: () => {
- const word = self.word.trim();
-
- return word ?
- item => {
- return (
- (item.item_data.title || '') .toLowerCase().indexOf(word) >= 0 ||
- (item.item_data.description || '') .toLowerCase().indexOf(word) >= 0 ||
- (item.description || '') .toLowerCase().indexOf(word) >= 0
- );
- } :
- () => true
- ;
- }
- });
- };
-
- const parseProps = () => {
- if (!location.hash || location.length <= 2) { return {}; }
- return location.hash.substring(1).split('+').reduce((map, entry) => {
- const [key, val] = entry.split('=').map(e => decodeURIComponent(e));
- map[key] = val;
- return map;
- }, {});
- };
-
- const update = () => {
- overrideFilter();
- const props = parseProps();
- // console.log('update form', props);
- render(props);
- };
-
- const init = () => {
- const _update = bounce.time(update, 100);
- _update();
- $('.content').on('nicoPageChanged', _update);
- };
-
- $(() => init());
- })(globalThis ? globalThis.window : window);
-