// ==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);