There are 2 previous versions of this script.
// ==UserScript==
// @name nicovideo Add Stars to Tags
// @namespace http://d.hatena.ne.jp/gifnksm/
// @description Add stars that represents their uploader set the tags unmodifiable.
// @include http://www.nicovideo.jp/watch/*
// ==/UserScript==
Array.prototype.include = function(x0) { return this.some(function(x) { return x == x0; }); }
function hasClassName(elem, className) {
return elem.className.split(/\s/).indexOf(className) != -1;
}
Array.prototype.toggle = function(x) {
var idx = this.indexOf(x);
if(idx == -1) {
this.push(x);
return null;
}
var temp = [], i;
for(i = 0; i < idx; i++)
temp.unshift(this.shift());
this.shift();
for(i = 0, len = temp.length; i < len; i++)
this.unshift(temp.shift());
return x;
}
function addClassName(elem, className) {
if(hasClassName(elem))
return null;
else
elem.className += ' ' + className;
}
function removeClassName(elem, className) {
elem.className = elem.className.split(/\s/).filter(function(exist_name) {
return exist_name != className;
}).join(' ');
}
function toggleClassName(elem, className) {
if(hasClassName(elem, className))
removeClassName(elem, className);
else
addClassName(elem, className);
}
function $exp(exp, ownerDocument) {
if(!ownerDocument) ownerDocument = document;
var resolver = document.createNSResolver(ownerDocument);
var def = (document.contentType == 'application/xhtml+xml') ? 'http://www.w3.org/1999/xhtml' : '';
return ownerDocument.createExpression(exp, function(prefix) {
return resolver.lookupNamespaceURI(prefix) || def;
});
}
const Single = {};
function $X(exp, context, type /* want type */) {
if (typeof context == 'function') {
type = context;
context = null;
}
if(typeof exp == 'string' || exp instanceof String)
exp = $exp(exp, context.ownerDocument || context);
switch (type) {
case String: return exp.evaluate(context, XPathResult.STRING_TYPE, null).stringValue;
case Number: return exp.evaluate(context, XPathResult.NUMBER_TYPE, null).numberValue;
case Boolean: return exp.evaluate(context, XPathResult.BOOLEAN_TYPE, null).booleanValue;
case Single: return exp.evaluate(context, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
case Array:
var result = exp.evaluate(context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var ret = [];
for (var i = 0, len = result.snapshotLength; i < len; i++)
ret.push(result.snapshotItem(i));
return ret;
case undefined:
var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
switch (result.resultType) {
case XPathResult.STRING_TYPE : return result.stringValue;
case XPathResult.NUMBER_TYPE : return result.numberValue;
case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
// not ensure the order.
var ret = [];
var i = null;
while (i = result.iterateNext())
ret.push(i);
return ret;
}
return null;
default:
throw(TypeError('$X: specified type is not valid type.'));
}
}
function $N(elem, attr, children) {
if(!elem) return null;
if(typeof elem == 'string' || elem instanceof String)
elem = document.createElement(elem);
else
elem = elem.cloneNode(!children);
for (key in attr) {
if (!attr.hasOwnProperty(key)) continue;
elem.setAttribute(key, attr[key]);
}
function recAppend(child) {
if(typeof child == 'string' || child instanceof String)
elem.appendChild(document.createTextNode(child));
else if(child instanceof Array)
child.forEach(recAppend);
else if(child)
elem.appendChild(child);
}
recAppend(children);
return elem;
}
const video_id = unsafeWindow.Video.id;
const request_url = 'http://www.nicovideo.jp/tag_edit/' + video_id;
const categoryTags = ["公式", "総合", "音楽", "エンターテイメント", "アニメ", "ゲーム", "ラジオ", "スポーツ", "科学", "料理", "政治", "動物", "歴史", "自然", "ニコニコ動画講座", "演奏してみた", "歌ってみた", "踊ってみた", "投稿者コメント", "日記", "アンケート", "チャット", "テスト", "台灣", "その他", "R-18"];
const lockedTags = unsafeWindow.Video.lockedTags;
const initialTags = unsafeWindow.Video.tags;
GM_addStyle(<><![CDATA[
._GM_tag_link_selected_and {
-moz-outline: 2px solid red;
-moz-outline-radius: 5px;
}
._GM_tag_link_selected_minus {
-moz-outline: 2px solid blue;
-moz-outline-radius: 5px;
}
]]></>);
var TagData = function(tagname) {
this.name = tagname;
this.is_locked = lockedTags.include(tagname);
this.is_category = categoryTags.include(tagname);
};
TagData.create = function(tagname) { return new TagData(tagname);};
TagData.prototype = {
get mark() {
if(typeof this._mark != "undefined")
return this._mark;
if(this.is_locked) {
this._mark = $N('span', {style: 'color:#F90;'}, '★');
if(this.is_category)
return this._mark = $N('strong', {style: 'color:#F30;'}, ['[', this._mark, ']']);
return this._mark;
}
if(this.is_category)
return this._mark = $N('strong', {style: 'color:#F30;'}, '[C]');
return this._mark = null;
},
get link() {
if(typeof this._link != "undefined")
return this._link;
// 以下はinitialTags やサーバから受信したデータから Tags を作った場合とかに必要となる。
if(!this.link_exp)
this.link_exp = $exp('//a[@rel = "tag" and text() = "' + this.name + '"]');
this._link = $X(this.link_exp, Tags.container, Single);
return this._link;
},
set link(link) { this._link = link; return this._link; },
decorateLink: function() {
if(this.link == null)
return;
if(this.mark != null)
this.link.parentNode.insertBefore(this.mark, this.link);
},
toString: function() {
return (this.is_locked? (this.is_category? '[★]' :' ★ ') : (this.is_category? '[C]' : ' ')) + this.name;
}
};
var selectedAndTags = [];
var selectedMinusTags = [];
document.addEventListener('mousedown', function(e) {
var link = e.target;
if(link.nodeName != 'A' || link.rel != 'tag')
return;
if(e.altKey && e.ctrlKey && e.shiftKey) {
e.preventDefault();
return;
}
}, false);
document.addEventListener('click', function(e) {
var link = e.target;
if(link.nodeName != 'A' || link.rel != 'tag')
return;
var tagname = link.href.replace(new RegExp("^http://www.nicovideo.jp/tag/"), "");
if(e.altKey && e.ctrlKey && e.shiftKey) {
removeClassName(link, '_GM_tag_link_selected_and');
toggleClassName(link, '_GM_tag_link_selected_minus');
selectedAndTags = selectedAndTags.filter(function(x) { return x != tagname; });
selectedMinusTags.toggle(tagname);
e.preventDefault();
GM_log('\n +: ' + selectedAndTags.map(decodeURI) + '\n -: ' + selectedMinusTags.map(decodeURI));
return;
}
if(e.altKey && e.ctrlKey) {
toggleClassName(link, '_GM_tag_link_selected_and');
removeClassName(link, '_GM_tag_link_selected_minus');
selectedMinusTags = selectedMinusTags.filter(function(x) { return x != tagname; });
selectedAndTags.toggle(tagname);
e.preventDefault();
GM_log('\n +: ' + selectedAndTags.map(decodeURI) + '\n -: ' + selectedMinusTags.map(decodeURI));
return;
}
if(selectedAndTags.length == 0 && selectedMinusTags.length == 0)
return;
var addr = selectedAndTags.join('+');
if(!selectedAndTags.include(tagname) && !selectedMinusTags.include(tagname))
addr += '+' + tagname;
addr = [addr].concat(selectedMinusTags).join(encodeURI(' -'))
var oldhref = link.href;
link.href = 'http://www.nicovideo.jp/tag/' + addr;
setTimeout(function() {
link.href = oldhref;
}, 500);
}, false);
//Tags = initialTags.map(TagData.create);
Tags = [];
Tags.reset = function() { this.length = 0; };
Tags.update = function() {
var len, j = 0, old_len = Tags.length;
var links = $X('//a[@rel = "tag"]/text()', Tags.container, Array);
if(links.length == 0) {
Tags.length = 0;
return;
}
links.forEach(function(text, i) {
// 新しく★を生成したりするのはコスト高そうなので、できるだけ再利用。
var tagname = text.nodeValue;
while(j < old_len && tagname != Tags[j].name)
j++;
if(j >= old_len)
Tags[i] = new TagData(tagname);
else {
Tags[i] = Tags[j];
j++;
}
Tags[i].link = text.parentNode;
len = i + 1;
});
Tags.length = len;
}
function appendStars(force_update) {
var div = document.getElementById("video_tags");
var p = $X('//p[contains(concat(" ", @class," "), " tag_txt ")]', div, Single);
if(p != null) {
Tags.container = p;
Tags.update();
Tags.forEach(function(tag) { tag.decorateLink(); });
var national_len = unsafeWindow.Video.tags.length;
var global_len = Tags.length - national_len;
div.getElementsByTagName('strong')[0].appendChild(
document.createTextNode(national_len + ((global_len > 0) ? ' + ' + global_len : '') + '件'));
}
else
p = div.getElementsByTagName('p')[0]; // タグを更新できなかったとき。(混雑しています、など)
p.appendChild(document.createTextNode(' '));
var reload_link = $N('a', {style: 'color: #F30;', href: 'javascript: void(0);'}, '【更新】');
reload_link.addEventListener('click', refreshTagLinks, false);
p.appendChild(reload_link);
}
var isGlobal = false;
function refreshTagLinks(callback, isAll) {
isAll = isAll || isGlobal;
var video_tags = document.getElementById('video_tags');
video_tags.innerHTML = '<img src="img/watch/tool_loading.gif" alt="処理中">';
selectedAndTags = [];
selectedMinusTags = [];
GM_xmlhttpRequest({
method: 'POST',
url: request_url,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
data: 'cmd=tags' + (isAll ? '&all=1' : ''),
onload: function(response) {
video_tags.innerHTML = response.responseText;
if(callback instanceof Function)
callback();
if (typeof unsafeWindow.Nicopedia != "undefined")
unsafeWindow.Nicopedia.decorateLinks();
appendStars();
}
});;
}
unsafeWindow.finishTagEdit = function(url, isAll) {
isGlobal = isAll;
var edit_form = document.getElementById('tag_edit_form');
if(edit_form)
edit_form.innerHTML = '<img src="img/watch/tool_loading.gif" alt="処理中">';
setTimeout(function() {
refreshTagLinks(function() {
document.getElementById('video_controls').style.display = '';
if(edit_form)
edit_form.parentNode.removeChild(edit_form);
}, isAll);}, 10);
}
unsafeWindow.showGlobalTags = function(url) {
unsafeWindow.finishTagEdit(url, true);
}
appendStars(false);
if(!unsafeWindow.User.isPremium)
GM_addStyle('.nicopedia_dic { display: inline; }');
// unsafeWindow.showGlobalTags('', true);
