Tools to interact with novelupdates.com site.
Versão de:
// ==UserScript==
// @name NUtools
// @namespace JDoe_NUtoolsV2
// @version 3
// @description Tools to interact with novelupdates.com site.
// @author John Doe
// @match http*://*.novelupdates.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery.serializeJSON/2.9.0/jquery.serializejson.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery-popup-overlay/1.7.13/jquery.popupoverlay.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_VERSION = 3;
const DEBUG_MODE = 0;
const defaultConfigs = {
cfg_move_to_list_id : 1,
cfg_move_req_confirm : 0,
cfg_cover_show_icon : 1,
cfg_move_reload : 1
};
const cssFiles = [
'https://fonts.googleapis.com/icon?family=Material+Icons',
'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css'
];
const htmlStyles = `
<style type="text/css">
/* Rules for sizing the icon. */
.material-icons.md-12 { font-size: 12px; }
.material-icons.md-18 { font-size: 18px; }
.material-icons.md-20 { font-size: 20px; }
.material-icons.md-24 { font-size: 24px; }
.material-icons.md-36 { font-size: 36px; }
.material-icons.md-48 { font-size: 48px; }
/* Rules for using icons as black on a light background. */
.material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
.material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
/* Rules for using icons as white on a dark background. */
.material-icons.md-light { color: rgba(255, 255, 255, 1); }
.material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
.material-icons {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
}
/* ################################ */
.js-nutools-hidden { display:none; }
.js-nutools-show-cover,.js-nutools-move-to-list { cursor: pointer; }
#js-nutools-settings-overlay {
-webkit-transform: scale(0.8);
-moz-transform: scale(0.8);
-ms-transform: scale(0.8);
transform: scale(0.8);
}
.popup_visible #js-nutools-settings-overlay {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
}
#js-nutools-settings-overlay fieldset {
border: 3px solid #1F497D;
background: #ddd;
border-radius: 2px;
padding: 5px;
margin-top: 30px;
}
#js-nutools-settings-overlay fieldset legend {
background: #1F497D;
color: #fff;
padding: 5px 20px ;
font-size: 20px;
border-radius: 5px;
box-shadow: 0 0 0 1px #ddd;
margin-left: 20px;
}
.js-nutools-well{
min-height:20px;
padding:19px;
margin-bottom:20px;
background-color:#f5f5f5;
border:1px solid #e3e3e3;
border-radius:4px;
-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);
box-shadow:inset 0 1px 1px rgba(0,0,0,.05)
}
.js-nutools-well blockquote{
border-color:#ddd;
border-color:rgba(0,0,0,.15)
}
.js-nutools-well-lg{
padding:24px;
border-radius:6px
}
.js-nutools-well-sm{
padding:9px;
border-radius:3px
}
</style>
`;
const htmlCPbutton = `
<div class="">
<p><button id="js-nutools-open-userscript-cp"><i class="material-icons md-18">settings</i>NUtools Settings</button></p>
<p><button id="js-nutools-get-language-button">Add language label</button></p>
</div>
`;
const htmlPageAppend = `
<div class="js-nutools-hidden">
<!-- NUtools overlay -->
<div id="js-nutools-move-confirm-overlay" class="js-nutools-well">
<div class="message">
Move '<b class="novel-title"></b>' to reading list [ <b class="reading-list-id"></b> ]?
</div>
<br><br>
<center>
<button type="button" class="js-nutools-move-confirm-overlay_close">Cancel</button>
<button type="button" id="js-nutools-move-confirm-overlay-move-button" data-reading-list-id="" data-novel-id="">Move</button>
</center>
</div>
<div id="js-nutools-get-language-confirm-overlay" class="js-nutools-well">
<div class="message">
<h3>Warning</h3>
<p>This action will create multiple requests to website and it is very intensive and take a long time to complete. Use it with extreme moderation.</p>
</div>
<br><br>
<center>
<button type="button" class="js-nutools-get-language-confirm-overlay_close">Cancel</button>
<button type="button" id="js-nutools-get-language-confirm-button">Get language</button>
</center>
</div>
<div id="js-nutools-cover-overlay" class="js-nutools-well">
</div>
<div id="js-nutools-settings-overlay" class="js-nutools-well">
<form id="settings_form">
<h3>Settings:</h3>
<fieldset>
<legend><i class="material-icons">format_indent_increase</i> Reading List</legend>
<p> Move to <a href="https://www.novelupdates.com/reading-list/">Reading List ID</a> :
<input type="text" name="cfg_move_to_list_id" placeholder="123" value="" pattern="[0-1]{1,3}" autocomplete="off" data-lpignore="true" title="the ID should only contain digits. e.g. 7" required>
</p>
<p>Require confirmation before moving to list? :<br>
<input type="radio" name="cfg_move_req_confirm" value="1"> Yes
<input type="radio" name="cfg_move_req_confirm" value="0" checked="checked"> No
</p>
<p>Reload page after moving:<br>
<input type="radio" name="cfg_move_reload" value="1"> Yes
<input type="radio" name="cfg_move_reload" value="0" checked="checked"> No
</p>
</fieldset>
<fieldset>
<legend><i class="material-icons">photo</i> Cover </legend>
</p>Show icon ? :<br>
<input type="radio" name="cfg_cover_show_icon" value="1"> Yes
<input type="radio" name="cfg_cover_show_icon" value="0" checked="checked"> No</li>
</p>
</fieldset>
<center>
<button type="button" class="js-nutools-settings-overlay_close">Close</button>
<!-- <button type="submit" class="js-nutools-settings-overlay_save">Save</button> -->
</center>
</form>
<style> input:invalid { border-color: #DD2C00; }</style>
</div>
<!-- /NUtools overlay -->
</div>
`;
let cfgs = {};
// functions
var storage = {
options : {
prefix : ''
},
// “Set” means “add if absent, replace if present.”
set : function(key, value) {
let storageVals = this.read(key);
if (typeof storageVals === 'undefined' || !storageVals) {
// add if absent
return this.add(key, value);
} else {
// replace if present
this.write(key, value);
return true;
}
},
// “Add” means “add if absent, do nothing if present” (if a uniquing collection).
add : function(key, value) {
let storageVals = this.read(key, false);
if (typeof storageVals === 'undefined' || !storageVals) {
this.write(key, value);
return true;
} else {
if (this._isArray(storageVals)) { // is array
let index = storageVals.indexOf(value);
if (index !== -1) {
// do nothing if present
return false;
} else {
// add if absent
storageVals.push(value);
this.write(key, storageVals);
return true;
}
} else if (this._isObject(storageVals)) { // is object
// merge obj value on obj
let result,
objToMerge = value;
result = Object.assign(storageVals, objToMerge);
this.write(key, result);
return false;
}
return false;
}
},
// “Replace” means “replace if present, do nothing if absent.”
replace : function(key, itemFind, itemReplacement) {
let storageVals = this.read(key, false);
if (typeof storageVals === 'undefined' || !storageVals) {
// do nothing if absent
return false;
} else {
if (this._isArray(storageVals)) { // is Array
let index = storageVals.indexOf(itemFind);
if (index !== -1) {
// replace if present
storageVals[index] = itemReplacement;
this.write(key, storageVals);
return true;
} else {
// do nothing if absent
return false;
}
} else if (this._isObject(storageVals)) {
// is Object
// replace property's value
storageVals[itemFind] = itemReplacement;
this.write(key, storageVals);
return true;
}
return false;
}
},
// “Remove” means “remove if present, do nothing if absent.”
remove : function(key, value) {
if (typeof value === 'undefined') { // remove key
this.delete(key);
return true;
} else { // value present
let storageVals = this.read(key);
if (typeof storageVals === 'undefined' || !storageVals) {
return true;
} else {
if (this._isArray(storageVals)) { // is Array
let index = storageVals.indexOf(value);
if (index !== -1) {
// remove if present
storageVals.splice(index, 1);
this.write(key, storageVals);
return true;
} else {
// do nothing if absent
return false;
}
} else if (this._isObject(storageVals)) { // is Object
let property = value;
delete storageVals[property];
this.write(key, storageVals);
return true;
}
return false;
}
}
},
get : function(key, defaultValue) {
return this.read(key, defaultValue);
},
// GM storage API
read : function(key, defaultValue) {
return this.unserialize(GM_getValue(this._prefix(key), defaultValue));
},
write : function(key, value) {
return GM_setValue(this._prefix(key), this.serialize(value));
},
delete : function(key) {
return GM_deleteValue(this._prefix(key));
},
readKeys : function() {
return GM_listValues();
},
// /GM Storage API
getAll : function() {
const keys = this._listKeys();
let obj = {};
for (let i = 0, len = keys.length; i < len; i++) {
obj[keys[i]] = this.read(keys[i]);
}
return obj;
},
getKeys : function() {
return this._listKeys();
},
getPrefix : function() {
return this.options.prefix;
},
empty : function() {
const keys = this._listKeys();
for (let i = 0, len = keys.lenght; i < len; i++) {
this.delete(keys[i]);
}
},
has : function(key) {
return this.get(key) !== null;
},
forEach : function(callbackFunc) {
const allContent = this.getAll();
for (let prop in allContent) {
callbackFunc(prop, allContent[prop]);
}
},
unserialize : function(value) {
if (this._isJson(value)) {
return JSON.parse(value);
}
return value;
},
serialize : function(value) {
if (this._isJson(value)) {
return JSON.stringify(value);
}
return value;
},
_listKeys : function(usePrefix = false) {
const prefixed = this.readKeys();
let unprefixed = [];
if (usePrefix) {
return prefixed;
} else {
for (let i = 0, len = prefixed.length; i < len; i++) {
unprefixed[i] = this._unprefix(prefixed[i]);
}
return unprefixed;
}
},
_prefix : function(key) {
return this.options.prefix + key;
},
_unprefix : function(key) {
return key.substring(this.options.prefix.length);
},
_isJson : function(item) {
try {
JSON.parse(item);
} catch (e) {
return false;
}
return true;
},
_isObject : function(a) {
return (!!a) && (a.constructor === Object);
},
_isArray : function(a) {
return (!!a) && (a.constructor === Array);
}
};
function isObject(val) {
if (val === null) {
return false;
}
return ((typeof val === 'function') || (typeof val === 'object'));
}
function setDebug(isDebug = false) {
if (isDebug) {
window.debug = window.console.log.bind(window.console, '%s: %s');
} else {
window.debug = function() {};
window.console.log = function() {};
}
}
function AddJQExternal(){
if ( !window.jQuery ){
var jq = document.createElement('script');
jq.type = 'text/javascript';
jq.src = 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js';
document.getElementsByTagName('head')[0].appendChild(jq);
//console.log("Added jQuery!");
} else {
//console.log("jQuery already exists.");
}
}
function AddExternalsToHEAD(arr = [] , forceExt = false){
for (var i = 0; i < arr.length; i++) {
var urlStr = arr[i];
var ext = (forceExt) ? forceExt : urlStr.slice((Math.max(0, urlStr.lastIndexOf('.')) || Infinity) + 1);
var ele = null;
switch( ext ) {
case 'js':
ele = document.createElement('script');
ele.type = 'text/javascript';
ele.src = urlStr;
break;
case 'css':
ele = document.createElement('link');
ele.rel = 'stylesheet';
ele.type = 'text/css';
ele.href = urlStr;
break;
default:
ele = document.createElement('script');
ele.type = 'text/javascript';
ele.src = urlStr;
}
document.getElementsByTagName('head')[0].appendChild( ele );
//console.log('Added '+ ext +' '+ urlStr);
}
}
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
function readConfig() {
let configs = storage.get('configs', defaultConfigs);
configs = Object.assign({}, defaultConfigs, configs);
debug('loading', JSON.stringify(configs));
return configs;
}
function saveConfig(args) {
let configs = {
cfg_cover_show_icon : args.cfg_cover_show_icon,
cfg_move_to_list_id : args.cfg_move_to_list_id,
cfg_move_req_confirm : args.cfg_move_req_confirm,
cfg_move_reload : args.cfg_move_reload
};
storage.set('configs', configs);
debug('saving', JSON.stringify(configs));
return configs;
}
function decodeHtml(html) {
var txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
// https://stackoverflow.com/questions/7298364/using-jquery-and-json-to-populate-forms
function populateForm($form, data) {
//console.log("PopulateForm, All form data: " + JSON.stringify(data));
$.each(data, function(key, value) // all json fields ordered by name
{
//console.log("Data Element: " + key + " value: " + value );
let $ctrls = $form.find('[name="'+key+'"]'); //all form elements for a name. Multiple checkboxes can have the same name, but different values
//console.log("Number found elements: " + $ctrls.length );
if ($ctrls.is('select')){//special form types
$('option', $ctrls).each(function() {
if (this.value == value) {
this.selected = true;
}
});
} else if ($ctrls.is('textarea')) {
$ctrls.val(value);
} else {
switch($ctrls.attr("type")) { //input type
case "text":
case "hidden":
$ctrls.val(value);
break;
case "radio":
if ($ctrls.length >= 1) {
//console.log("$ctrls.length: " + $ctrls.length + " value.length: " + value.length);
$.each($ctrls, function(index)
{ // every individual element
let elemValue = $(this).attr("value");
let singleVal = value;
let elemValueInData = singleVal;
if (elemValue===value) {
$(this).prop('checked', true);
} else {
$(this).prop('checked', false);
}
});
}
break;
case "checkbox":
if ($ctrls.length > 1) {
//console.log("$ctrls.length: " + $ctrls.length + " value.length: " + value.length);
$.each($ctrls,function(index) { // every individual element
let elemValue = $(this).attr("value");
let elemValueInData;
let singleVal;
for (var i=0; i<value.length; i++){
singleVal = value[i];
console.log("singleVal : " + singleVal + " value[i][1]" + value[i][1] );
if (singleVal === elemValue){
elemValueInData = singleVal;
}
}
if (elemValueInData){
//console.log("TRUE elemValue: " + elemValue + " value: " + value);
$(this).prop('checked', true);
//$(this).prop('value', true);
} else {
//console.log("FALSE elemValue: " + elemValue + " value: " + value);
$(this).prop('checked', false);
//$(this).prop('value', false);
}
});
} else if($ctrls.length == 1) {
$ctrl = $ctrls;
if (value) {
$ctrl.prop('checked', true);
} else {
$ctrl.prop('checked', false);
}
}
break;
} //switch input type
} // if/else
}); // all json fields
} // populate form
// end functions
setDebug(DEBUG_MODE);
// AddJQExternal();
AddExternalsToHEAD(cssFiles, 'css');
storage.options.prefix = 'nutools_';
toastr.options = {
"closeButton" : false,
"debug" : false,
"newestOnTop" : false,
"progressBar" : false,
"positionClass" : "toast-top-right",
"preventDuplicates" : false,
"onclick" : null,
"showDuration" : "300",
"hideDuration" : "1000",
"timeOut" : "5000",
"extendedTimeOut" : "1000",
"showEasing" : "swing",
"hideEasing" : "linear",
"showMethod" : "fadeIn",
"hideMethod" : "fadeOut"
};
cfgs = readConfig();
$('head').append( htmlStyles );
$('body').append( htmlPageAppend );
$('.l-content').prepend( htmlCPbutton );
setTimeout(function() {
let ln_rows = 0;
let ln_series_urls = {};
$('td[class^="sid"]').each(function(){
let str = $(this).attr('class');
let id = parseInt( str.replace('sid', '') );
let title = $(this).find('a').attr('title');
let surl = $(this).find('a').attr('href');
let tr = $(this).parent().closest('tr');
let html = '';
let html_cover = (cfgs.cfg_cover_show_icon == 1) ? '<span class="js-nutools-show-cover" data-novel-url="'+ surl +'" title="Show Cover"><i class="material-icons md-18">photo</i></span> ' : '';
let html_moveToList = ' <span class="js-nutools-move-to-list" data-novel-id="'+ id +'" data-novel-title="'+ title +'" title="Move to list"><i class="material-icons md-18">format_indent_increase</i></span> ';
let html_lang = ' <span class="js-nutools-lang" data-novel-id="'+ id +'" ></span>';
tr.attr('data-novel-id', id);
$(this).prepend('<span class="js-nutools-wrap" data-novel-id="'+ id +'" data-novel-title="'+ title +'">' + html + html_cover + html_moveToList + html_lang +'</span> ');
ln_rows++;
//if (ln_rows < 4)
ln_series_urls[ id ] = surl;
});
debug('page nl rows', ln_rows );
// Event handlers
$('.js-nutools-show-cover').click(function(){
let url = $(this).attr('data-novel-url');
$.ajax({
url: url,
success: function(newHTML, textStatus, jqXHR) {
let img_html = $(newHTML).find('.seriesimg img, .serieseditimg img').first();
$('#js-nutools-cover-overlay').html( img_html ).popup('show');
},
error: function(jqXHR, textStatus, errorThrown) {
}
});
});
$('.js-nutools-move-to-list').click(function(){
let id = parseInt( $(this).attr('data-novel-id') );
let title = $(this).attr('data-novel-title');
let url = 'https://www.novelupdates.com/updatelist.php?lid='+ cfgs.cfg_move_to_list_id +'&act=move&sid='+ id;
if (cfgs.cfg_move_req_confirm == 1) {
let message = "Move '"+ title +"' to the reading list [ "+ cfgs.cfg_move_to_list_id +" ]?";
$('#js-nutools-move-confirm-overlay .novel-title').html( title );
$('#js-nutools-move-confirm-overlay .reading-list-id').html( cfgs.cfg_move_to_list_id );
$('#js-nutools-move-confirm-overlay-move-button').attr('data-novel-id', id);
$('#js-nutools-move-confirm-overlay').popup({
color : 'white',
opacity : 1,
transition : '0.3s',
scrolllock : true,
blur : false
});
$('#js-nutools-move-confirm-overlay').popup('show');
} else {
$.get(url, function( data ) {
debug('ajax get', url );
toastr.success('Novel moved to list');
});
if (cfgs.cfg_move_reload == 1) {
location.reload();
}
}
});
$('body').on('click', '#js-nutools-move-confirm-overlay-move-button', function(){
let id = parseInt( $(this).attr('data-novel-id') );
let title = $(this).attr('data-novel-title');
let url = 'https://www.novelupdates.com/updatelist.php?lid='+ cfgs.cfg_move_to_list_id +'&act=move&sid='+ id;
$.get(url, function( data ) {
debug('ajax get', url );
// $('tr[data-novel-id="'+ id +'"]').addClass('js-nutools-hidden');
$('#js-nutools-move-confirm-overlay').popup('hide');
if (cfgs.cfg_move_reload == 1) {
location.reload();
}
});
});
$('#js-nutools-get-language-button').click(function(){
$('#js-nutools-get-language-confirm-overlay').popup({
color : 'white',
opacity : 0.5,
transition : '0.3s',
scrolllock : true,
blur : false
});
$('#js-nutools-get-language-confirm-overlay').popup('show');
});
$('#js-nutools-get-language-confirm-button').click(function(){
let results = [];
let deferreds = [];
let novels_lang = [];
$('#js-nutools-get-language-confirm-overlay').popup('hide');
for(var key in ln_series_urls) {
let id = key;
let url = ln_series_urls[key];
let deferred =
$.ajax(url, {
success: function(html) {
let lang = '';
results.push(html);
lang = $(html).find('#showtype span').first().text();
if(lang !='') {
lang.replace(/()/gi, '');
} else {
lang = 'N/A';
}
novels_lang[ id ] = lang;
}
});
deferreds.push(deferred);
}
$.when.apply($, deferreds).then(function() {
for(var key in novels_lang) {
$('.js-nutools-lang[data-novel-id="'+ key +'"]').html('<b>'+ novels_lang[key] +'</b>');
}
});
});
$('#js-nutools-open-userscript-cp').click(function(){
$('#js-nutools-settings-overlay').popup('show');
let form = $('#settings_form');
populateForm(form, cfgs);
});
$('.js-nutools-settings-overlay_save').click(function(){
event.preventDefault();
let args = $('#settings_form').serializeJSON();
cfgs = saveConfig( args );
$('#js-nutools-settings-overlay').popup('hide');
location.reload();
});
$('#settings_form').on('focusin', 'input', function(){
//$(this).data('val', $(this).val());
}).on('change','input', function(){
let args = $('#settings_form').serializeJSON();
cfgs = saveConfig( args );
});
$('#js-nutools-settings-overlay').popup({
color : 'white',
opacity : 1,
transition : '0.3s',
scrolllock : true,
blur : false
});
}, 100); // milisec
})();