Cleanreads

Cleanreads userscript for Goodreads.com

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Cleanreads
// @namespace    http://hermanfassett.me
// @version      1.4
// @description  Cleanreads userscript for Goodreads.com
// @author       Herman Fassett
// @match        https://www.goodreads.com/*
// @grant        GM_addStyle
// ==/UserScript==

GM_addStyle( `
    .contentComment { padding: 10px 5px 10px 5px; }
    .contentClean { color: green; }
    .contentNotClean { color: red; }
    .contentUnknown { color: blue; }
    #crSettingsDialog {
        width: 500px;
        height: 500px;
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: white;
        border: 1px solid rgba(0,0,0,0.15);
        display: none;
    }
    #crSettingsHeader {
        height: 50px;
        width: 100%;
        background: #F4F1EA;
        text-align: center;
        box-shadow: 0 1px 2px rgba(0,0,0,0.15);
    }
    #crSettingsHeader h1 {
        line-height: 50px;
        color: #382110;
    }
    #crSettingsHeader h1, .crSettingsHeader {
        font-family: "Lato", "Helvetica Neue", "Helvetica", sans-serif;
    }
    .crSettingsHeader, #crSettingsTermButtons { padding-top: 20px; }
    #crSettingsTermButtons button { margin-right: 5px; }
    #crSettingsBody { height: 400px; overflow: auto; }
    #crSettingsFooter {
        height: 50px;
        width: 100%;
        box-shadow: 1px 0 2px rgba(0,0,0,0.15);
    }
    #crSettingsFooter button {
        float: right;
        margin: 10px 10px 0 0;
    }
    #crSettingsFooter button.saveButton {
        color: white;
        background-color: #409D69;
    }
    .crTermsContainer { display: inline-block; }
    #crSnippetHeader {
        float: left;
        padding-right: 10px;
    }
`);

(function(Cleanreads) {
    'use strict';

    /** The group bookshelf ID to use as default clean check list */
    Cleanreads.CLEAN_READS_BOOKSHELF_ID = 5989;

    /** The positive search terms when determining verdict */
    Cleanreads.POSITIVE_SEARCH_TERMS = [
        { term: 'clean', exclude: { before: ['not', 'isn\'t'], after: ['ing'] }},
        { term: 'no sex', exclude: { before: [], after: [] }}
    ];

    /** The negative search terms when determining verdict */
    Cleanreads.NEGATIVE_SEARCH_TERMS = [
        { term: 'sex', exclude: { before: ['no'], after: ['ist'] }},
        { term: 'adult', exclude: { before: ['young', 'new'], after: ['hood', 'ing']}},
        { term: 'erotic', exclude: { before: ['not', 'isn\'t'], after: []}}
    ];

    Cleanreads.SNIPPET_HALF_LENGTH = 65;
    Cleanreads.ATTEMPTS = 10;

    /**
     * Load the settings from local storage if existant
     */
    Cleanreads.loadSettings = function() {
        try {
            Cleanreads.POSITIVE_SEARCH_TERMS = JSON.parse(localStorage.getItem("Cleanreads.POSITIVE_SEARCH_TERMS")) || Cleanreads.POSITIVE_SEARCH_TERMS;
            Cleanreads.NEGATIVE_SEARCH_TERMS = JSON.parse(localStorage.getItem("Cleanreads.NEGATIVE_SEARCH_TERMS")) || Cleanreads.NEGATIVE_SEARCH_TERMS;
            Cleanreads.SNIPPET_HALF_LENGTH = JSON.parse(localStorage.getItem("Cleanreads.SNIPPET_HALF_LENGTH")) || Cleanreads.SNIPPET_HALF_LENGTH;
            Cleanreads.ATTEMPTS = JSON.parse(localStorage.getItem("Cleanreads.ATTEMPTS")) || Cleanreads.ATTEMPTS;
            Cleanreads.CLEAN_READS_BOOKSHELF = JSON.parse(localStorage.getItem("Cleanreads.CLEAN_READS_BOOKSHELF")) || {
                books: [],
                timestamp: new Date(0),
                unloaded: true
            };

            // Get Clean Reads shelf clean books if not recently loaded (1 day)
            let now = new Date();
            if (now.setDate(now.getDate() - 1) > new Date(Cleanreads.CLEAN_READS_BOOKSHELF.timestamp)) {
                Cleanreads.getGroupBookshelfBooks(Cleanreads.CLEAN_READS_BOOKSHELF_ID, 5000)
                .then(data=> {
                    Cleanreads.CLEAN_READS_BOOKSHELF = {
                        books: data,
                        timestamp: new Date()
                    };
                    localStorage.setItem("Cleanreads.CLEAN_READS_BOOKSHELF", JSON.stringify(Cleanreads.CLEAN_READS_BOOKSHELF));
                })
                .finally(Cleanreads.searchBookshelf);
            }

            let settingsBody = document.getElementById("crSettingsBody");
            if (settingsBody) {
                settingsBody.innerHTML = `
                <div class="userInfoBoxContent">
                    <div id="crSettingsTermButtons">
                    </div>
                    <h1 class="crSettingsHeader">Positive Search Terms:</h1>
                    <div id="crPositiveSearchTerms">
                    </div>
                    <h1 class="crSettingsHeader">Negative Search Terms:</h1>
                    <div id="crNegativeSearchTerms">
                    </div>
                    <h1 class="crSettingsHeader">Other Settings:</h1>
                    <h4 id="crSnippetHeader">Snippet length:</h4> <input id="crSnippetHalfLength" type="number" value="${Cleanreads.SNIPPET_HALF_LENGTH}" min="0" />
                    <h4 id="crAttemptsHeader">Max Verdict Load Attempts (tries every second):</h4> <input id="crAttempts" type="number" value="${Cleanreads.ATTEMPTS}" min="1" />
                </div>
                `;

                // Add buttons
                let addPositiveButton = document.createElement("button");
                addPositiveButton.innerText = "Add Positive";
                addPositiveButton.className = "gr-button";
                addPositiveButton.onclick = Cleanreads.addSearchTerm.bind(null, true, null, null, null);
                document.getElementById("crSettingsTermButtons").appendChild(addPositiveButton);
                let addNegativeButton = document.createElement("button");
                addNegativeButton.innerText = "Add Negative";
                addNegativeButton.className = "gr-button";
                addNegativeButton.onclick = Cleanreads.addSearchTerm.bind(null, false, null, null, null);
                document.getElementById("crSettingsTermButtons").appendChild(addNegativeButton);
                let resetButton = document.createElement("button");
                resetButton.innerText = "Reset";
                resetButton.className = "gr-button";
                resetButton.onclick = function() {
                    if (confirm("Are you sure you want to remove? You will have to refresh the page to see default values loaded.")) {
                        localStorage.removeItem("Cleanreads.POSITIVE_SEARCH_TERMS");
                        localStorage.removeItem("Cleanreads.NEGATIVE_SEARCH_TERMS");
                        localStorage.removeItem("Cleanreads.SNIPPET_HALF_LENGTH");
                        localStorage.removeItem("Cleanreads.ATTEMPTS");
                        Cleanreads.loadSettings();
                    }
                }
                document.getElementById("crSettingsTermButtons").appendChild(resetButton);

                // Add existing terms
                Cleanreads.POSITIVE_SEARCH_TERMS.forEach((search) => Cleanreads.addSearchTerm(true, search.term, search.exclude.before, search.exclude.after));
                Cleanreads.NEGATIVE_SEARCH_TERMS.forEach((search) => Cleanreads.addSearchTerm(false, search.term, search.exclude.before, search.exclude.after));
            }
        } catch (ex) {
            console.error("Cleanreads: Failed to load settings!", ex);
        }
    };

    /**
     * Save the positive and negative search terms to local storage
     */
    Cleanreads.saveSettings = function() {
        let positiveTerms = document.querySelectorAll("#crPositiveSearchTerms > .crTermsContainer");
        let negativeTerms = document.querySelectorAll("#crNegativeSearchTerms > .crTermsContainer");

        Cleanreads.POSITIVE_SEARCH_TERMS = [...positiveTerms].map((search) => {
            return {
                term: search.querySelector("[name=term]").value,
                exclude: {
                    before: search.querySelector("[name=excludeBefore]").value.split(",").map(x => x.trim()),
                    after: search.querySelector("[name=excludeAfter]").value.split(",").map(x => x.trim())
                }
            }
        }).filter(x => x.term);

        Cleanreads.NEGATIVE_SEARCH_TERMS = [...negativeTerms].map((search) => {
            return {
                term: search.querySelector("[name=term]").value,
                exclude: {
                    before: search.querySelector("[name=excludeBefore]").value.split(",").map(x => x.trim()),
                    after: search.querySelector("[name=excludeAfter]").value.split(",").map(x => x.trim())
                }
            }
        }).filter(x => x.term);

        Cleanreads.SNIPPET_HALF_LENGTH = parseInt(document.getElementById("crSnippetHalfLength").value) || Cleanreads.SNIPPET_HALF_LENGTH;
        Cleanreads.ATTEMPTS = parseInt(document.getElementById("crAttempts").value) || Cleanreads.ATTEMPTS;

        localStorage.setItem("Cleanreads.POSITIVE_SEARCH_TERMS", JSON.stringify(Cleanreads.POSITIVE_SEARCH_TERMS));
        localStorage.setItem("Cleanreads.NEGATIVE_SEARCH_TERMS", JSON.stringify(Cleanreads.NEGATIVE_SEARCH_TERMS));
        localStorage.setItem("Cleanreads.SNIPPET_HALF_LENGTH", JSON.stringify(Cleanreads.SNIPPET_HALF_LENGTH));
        localStorage.setItem("Cleanreads.ATTEMPTS", JSON.stringify(Cleanreads.ATTEMPTS));
        Cleanreads.loadSettings();
    }

    /**
     * Setup the settings modal for Cleanreads
     */
    Cleanreads.setupSettings = function() {
        // Add link to menu dropdown
        let links = Array.from(document.getElementsByClassName('menuLink')).filter(x => x.innerText == 'Account settings');
        if (links && links.length) {
            let li = document.createElement('li');
            li.className = 'menuLink';
            li.onclick = Cleanreads.showSettings;
            li.innerHTML = `<a href='#' class='siteHeader__subNavLink'>Cleanreads settings</a>`;
            links[0].parentNode.insertBefore(li, links[0].nextSibling);
        }
        // Add dialog
        document.body.innerHTML += `
            <div id="crSettingsDialog">
                <div id="crSettingsHeader"><h1>Cleanreads Settings</h1></div>
                <div id="crSettingsBody">
                </div>
                <div id="crSettingsFooter"></div>
            </div>
            `;
        // Add link to profile page
        let settingsLink = document.createElement('a');
        settingsLink.href = '#';
        settingsLink.innerText = 'Cleanreads settings';
        settingsLink.onclick = Cleanreads.showSettings;
        document.getElementsByClassName('userInfoBoxContent')[0].appendChild(settingsLink);
        // Add close button to dialog
        let closeButton = document.createElement('button');
        closeButton.innerText = 'Close';
        closeButton.className = 'gr-button';
        closeButton.onclick = Cleanreads.hideSettings;
        document.getElementById('crSettingsFooter').appendChild(closeButton);
        // Add save button to dialog
        let saveButton = document.createElement('button');
        saveButton.innerText = 'Save';
        saveButton.className = 'gr-button saveButton';
        saveButton.onclick = Cleanreads.saveSettings;
        document.getElementById('crSettingsFooter').appendChild(saveButton);
        Cleanreads.loadSettings();
    };

    /**
     * Add a search term to the settings UI
     */
    Cleanreads.addSearchTerm = function(positive, term, before, after) {
        document.getElementById(`cr${positive ? 'Positive' : 'Negative'}SearchTerms`).insertAdjacentHTML("beforeend",
            `<div class="crTermsContainer">
             <input name="excludeBefore" value="${before ? before.join(", ") : ''}" type="text" />
             <input name="term" value="${term || ''}" type="text" />
             <input name="excludeAfter" value="${after ? after.join(", ") : ''}" type="text" />
             </div>`);
    };

    /**
     * Setup the rating (verdict) container on a book page
     */
    Cleanreads.setupRating = function() {
        let match = window.location.pathname.match(/book\/show\/(\d+)/);
        if (match && match.length > 1) {
            Cleanreads.bookId = window.location.pathname.match(/show\/(\d*)/)[1];
            Cleanreads.loadSettings();
            Cleanreads.reviews = [];
            Cleanreads.shelves = [];
            Cleanreads.positives = 0;
            Cleanreads.negatives = 0;

            // Create container for rating
            let container = document.getElementById('descriptionContainer');
            let contentDescription = document.createElement('div');
            contentDescription.id = 'contentDescription';
            contentDescription.className = 'readable stacked u-bottomGrayBorder u-marginTopXSmall u-paddingBottomXSmall';
            contentDescription.innerHTML = `
                <h2 class="buyButtonContainer__title u-inlineBlock">Cleanreads Rating</h2>
                <h2 class="buyButtonContainer__title">
                Verdict: <span id="crVerdict">Loading...</span>
                (<span id="crPositives" class="contentClean">0</span>/<span id="crNegatives" class="contentNotClean">0</span>)
                </h2>
                <a id='expandCrDetails' href="#">(Details)</a>
                <div id="crDetails" style="display:none"></div>
                `;
            container.parentNode.insertBefore(contentDescription, container.nextSibling);
            Cleanreads.crDetails = document.getElementById('crDetails');
            document.getElementById('expandCrDetails').onclick = Cleanreads.expandDetails;

            Cleanreads.getTopBookShelves(Cleanreads.bookId).then(shelves => {
                Cleanreads.shelves = shelves;
                Cleanreads.startReviews();
            }).catch(err => Cleanreads.startReviews());
        }
    };

    /**
     * Start attempting to get the available reviews on the page and read their content
     */
    Cleanreads.startReviews = function() {
        Cleanreads.getReviews();
        // Reviews are delayed content so keep looking for a bit if nothing
        if (!Cleanreads.reviews.length && Cleanreads.ATTEMPTS--) {
            setTimeout(Cleanreads.startReviews, 1000);
        } else {
            Cleanreads.calculateContent();
        }
    };

    /**
     * Get reviews from page (only gets the first page of reviews, not easy to access others without API)
     */
    Cleanreads.getReviews = function() {
        let reviewElements = document.querySelectorAll('#reviews .reviewText');
        Cleanreads.reviews = Array.from(reviewElements).map(x => (x.querySelector('[style]') || x).innerText.trim());
    };

    /**
     * Get title as text with series appended
     */
    Cleanreads.getTitle = function() {
        return document.getElementById('bookTitle').innerText.trim() + document.getElementById('bookSeries').innerText.trim();
    };

    /**
     * Get book description text
     */
    Cleanreads.getDescription = function() {
        let description = document.getElementById('description');
        return (description.querySelector('[style]') || description).innerText.trim();
    };

    /**
     * Get group bookshelf titles
     * @param {string} shelfId - The bookshelf id
     * @param {number} maxCount - The maximum number of books in the bookshelf to return
     * @returns {Promise} - A promise that resolves to array of book ids or rejects with error
     */
    Cleanreads.getGroupBookshelfBooks = function(shelfId, maxCount) {
        return new Promise(function(resolve, reject) {
            jQuery.ajax(`${window.location.origin}/group/bookshelf/${shelfId}?utf8=✓&view=covers&per_page=${maxCount || 1000}`)
            .done(result => {
                resolve(jQuery(result).find(".rightContainer div > a").toArray().map(x => (x.href.match(/show\/(\d*)/)||[])[1]));
            })
            .fail(err => reject(err));
        });
    };

    /**
     * Get the top 100 shelf names for a given book
     * @param {string} bookId - The book id
     * @returns {Promise} - A promise that resolves to array of top 100 shelves for given book 
     */
    Cleanreads.getTopBookShelves = function(bookId) {
        return new Promise(function(resolve, reject) {
            jQuery.ajax(`${window.location.origin}/book/shelves/${bookId}`)
            .done(result => {
                resolve(jQuery(result).find('.shelfStat').toArray().map(x => `${jQuery(x).find('.actionLinkLite').text().replace(/-/gi, ' ')} (${jQuery(x).find('.smallText').text().trim()})`));
            })
            .fail(err => reject(err));
        });
    };

    /**
     * Get list titles
     * TODO: currently only gets first page
     * @param {string} listId - The list id
     * @returns {Promise} - A promise that resolves to array of book ids or rejects with error
     */
    Cleanreads.getListBooks = function(listId) {
        return new Promise(function(resolve, reject) {
            jQuery.ajax(`${window.location.origin}/list/show/${listId}`)
            .done(result => {
                resolve(jQuery(result).find(".tableList tr td:nth-child(2) div:nth-child(1)").toArray().map(x => x.id))
            })
            .fail(err => {
                reject(err);
            });
        });
    };

    /**
     * Calculate the cleanliness
     */
    Cleanreads.calculateContent = function() {
        let count = 0, containing = [];
        // Insert containers for bases
        Cleanreads.crDetails.innerHTML += 
        `<h2 class="buyButtonContainer__title u-marginTopXSmall">Bookshelf Content Basis: </h2>
        <div id="bookshelfBasis">
            <i class="contentComment">
                Loading
                <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
            </i>
        </div>`;
        Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Description Content Basis: </h2><div id="descriptionBasis"></div>`;
        Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Clean Basis: </h2><div id="cleanBasis"></div>`;
        Cleanreads.crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Not Clean Basis: </h2><div id="notCleanBasis"></div>`;
        
        // Get containers
        let descriptionBasis = document.getElementById('descriptionBasis'),
            cleanBasis = document.getElementById('cleanBasis'),
            notCleanBasis = document.getElementById('notCleanBasis');

        // Search description
        let description = `Title: ${Cleanreads.getTitle()}\nDescription: ${Cleanreads.getDescription()}`;
        Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, [description], descriptionBasis, true, Cleanreads.insertComment);
        Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, [description], descriptionBasis, false, Cleanreads.insertComment);

        // Search top shelves
        Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, Cleanreads.shelves, cleanBasis, true, Cleanreads.insertShelf);
        Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, Cleanreads.shelves, notCleanBasis, false, Cleanreads.insertShelf);

        // Search reviews
        Cleanreads.searchContent(Cleanreads.POSITIVE_SEARCH_TERMS, Cleanreads.reviews, cleanBasis, true, Cleanreads.insertComment);
        Cleanreads.searchContent(Cleanreads.NEGATIVE_SEARCH_TERMS, Cleanreads.reviews, notCleanBasis, false, Cleanreads.insertComment);

        // Fill bases if nothing
        if (!descriptionBasis.innerHTML) {
            descriptionBasis.innerHTML = '<i class="contentComment">None</i>';
        }
        if (!cleanBasis.innerHTML) {
            cleanBasis.innerHTML = '<i class="contentComment">None</i>';
        }
        if (!notCleanBasis.innerHTML) {
            notCleanBasis.innerHTML = '<i class="contentComment">None</i>';
        }

        // Update Clean Reads verdict
        if (!Cleanreads.CLEAN_READS_BOOKSHELF.unloaded) {
            Cleanreads.updateVerdict();
            Cleanreads.searchBookshelf();
        }
    };

    /**
     * Function to search for terms in a given string
     * @param {term} term - Term object to match in content
     * @param {string} content - Content to search
     * @returns {Array} - RegExp result array
     */
    Cleanreads.matchTerm = function(term, content) {
        let regex = new RegExp(`(^|[^(${term.exclude.before.join`|`}|\\s*)])(\\W*)(${term.term})(\\W*)($|[^(${term.exclude.after.join`|`}|\\s*)])`);
        let contentMatch = content.toLowerCase().match(regex);
        return contentMatch;
    }

    /**
     * Search string array for given list of terms, add matches to given container, and increment positive/negative verdict
     * @param {term object array} terms - Terms to search for
     * @param {string array} contents - Contents to search
     * @param {element} container - Result container
     * @param {boolean} positive - Flag if positive or negative search term to determine result
     * @param {function} insertFunction - Function to append result to container
     */
    Cleanreads.searchContent = function(terms, contents, container, positive, insertFunction) {
        contents.forEach(content => {
            terms.forEach(term => {
                let contentMatch = Cleanreads.matchTerm(term, content);
                if (!!contentMatch) {
                    positive ? Cleanreads.positives++ : Cleanreads.negatives++;
                    let index = contentMatch.index + contentMatch[1].length + contentMatch[2].length;
                    insertFunction(content, contentMatch[3], index, positive, container);
                }
            });
        })
    };

    /** Insert a matched comment into given container */
    Cleanreads.insertComment = function(content, term, index, positive, container) {
        container.innerHTML += `
            <div class="contentComment">
                ...${content.slice(index - Cleanreads.SNIPPET_HALF_LENGTH, index)}<b class="content${positive ? '' : 'Not'}Clean">${
                    content.substr(index, term.length)
                }</b>${content.slice(index + term.length, index + Cleanreads.SNIPPET_HALF_LENGTH)}...
            </div>
        `;
    };

    /** Insert a matched shelf into given container */
    Cleanreads.insertShelf = function(content, term, index, positive, container) {
        container.innerHTML += `
            <div class="contentComment">
                Shelved as: ${content.slice(0, index)}<b class="content${positive ? '' : 'Not'}Clean">${
                    content.substr(index, term.length)
                }</b>${content.slice(index + term.length)}
            </div>
        `;
    };

    /**
     * Search the loaded bookshelf book ids for current book and update verdict
     */
    Cleanreads.searchBookshelf = function() {
        let bookId = window.location.pathname.match(/show\/(\d*)/)[1];
        let bookshelfBasis = document.getElementById('bookshelfBasis');
        if (bookId && Cleanreads.CLEAN_READS_BOOKSHELF.books.indexOf(bookId) != -1) {
            bookshelfBasis.innerHTML = 
            `<div class="contentClean">
                Found in 
                <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
            </div>`;
            Cleanreads.positives++;
            Cleanreads.updateVerdict(true);
        } else {
            bookshelfBasis.innerHTML = 
            `<div class="contentNotClean">
                Not found in 
                <a href="${window.location.origin}/group/bookshelf/${Cleanreads.CLEAN_READS_BOOKSHELF_ID}">Clean Reads bookshelf</a>
            </div>`;
            Cleanreads.updateVerdict();
        }
    };

    /**
     * Update the verdict shown in UI on the book
     * @param {boolean} overrideClean - If true, always set clean, but preserve positive/negative count
     */
    Cleanreads.updateVerdict = function(overrideClean) {
        let verdict = document.getElementById('crVerdict');
        if (overrideClean || (Cleanreads.positives && Cleanreads.positives > Cleanreads.negatives)) {
            verdict.innerText = `${Cleanreads.negatives ? 'Probably' : 'Most likely'} clean`;
            verdict.className += 'contentClean';
        } else if (Cleanreads.negatives && Cleanreads.negatives > Cleanreads.positives) {
            verdict.innerText = `${Cleanreads.positives ? 'Probably' : 'Most likely'} not clean`;
            verdict.className += 'contentNotClean';
        } else {
            verdict.innerText = Cleanreads.positives && Cleanreads.negatives ? 'Could be clean or not clean' : 'Unknown';
            verdict.className += 'contentUnknown';
        }
        document.getElementById('crPositives').innerText = Cleanreads.positives;
        document.getElementById('crNegatives').innerText = Cleanreads.negatives;
    };

    /**
     * Expand the details section of Cleanreads verdict
     */
    Cleanreads.expandDetails = function() {
        let collapsedText = '(Details)',
            expandedText = '(Hide)';
        if (this.innerText == collapsedText) {
            Cleanreads.crDetails.style.display = 'block';
            this.innerText = expandedText;
        } else if (this.innerText == expandedText) {
            Cleanreads.crDetails.style.display = 'none';
            this.innerText = collapsedText;
        }
    };

    /**
     * Show the settings modal for Cleanreads
     */
    Cleanreads.showSettings = function() {
        document.getElementById("crSettingsDialog").style.display = 'block';
        return false;
    };

    /**
     * Hide the settings modal for Cleanreads
     */
    Cleanreads.hideSettings = function() {
        document.getElementById("crSettingsDialog").style.display = 'none';
        return false;
    };

    // Loading. If on a book load the verdict, else if on a user page load settings
    if (window.location.href.match("/book/")) {
        Cleanreads.setupRating();
    } else if (window.location.href.match("/user/")) {
        Cleanreads.setupSettings()
    }
})(window.Cleanreads = window.Cleanreads || {});