NUtools

Tools to interact with novelupdates.com site.

As of 2018-02-27. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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