Cleanreads

Cleanreads userscript for Goodreads.com

当前为 2018-10-06 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

GM_addStyle( `
    .contentComment {
        padding: 10px 5px 10px 5px;
    }
    .contentClean {
        color: green;
    }
    .contentNotClean {
        color: red;
    }
    .contentUnknown {
        color: blue;
    }
`);

(function() {
    'use strict';

    // Search terms. Strings or regex
    const POSITIVE_SEARCH_TERMS = ['clean', 'no sex'];
    const NEGATIVE_SEARCH_TERMS = [/[^(!no)](^|\s)sex(y|\s|$|\W)/, /[^(young)](^|\s)adult(\s|$|\W)/];

    let reviews = [], attempts = 10, positives = 0, negatives = 0;
    let match = window.location.pathname.match(/show\/(\d+)/);
    if (match && match.length > 1) {
        let id = match[1];
        getReviews(id);
    }

    // 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);
    let crDetails = document.getElementById('crDetails');
    document.getElementById('expandCrDetails').onclick = expandDetails;

    function start() {
        getReviews();
        // Reviews are delayed content so keep looking for a bit if nothing
        if (!reviews.length && attempts--) {
            setTimeout(start, 1000);
        } else {
            calculateContent();
        }
    }

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

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

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

    // Calculate the cleanliness
    function calculateContent() {
        let count = 0, containing = [];
        // Insert containers for bases
        crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Description Content Basis: </h2><div id="descriptionBasis"></div>`;
        crDetails.innerHTML += `<h2 class="buyButtonContainer__title u-marginTopXSmall">Clean Basis: </h2><div id="cleanBasis"></div>`;
        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: ${getTitle()}\nDescription: ${getDescription()}`;
        POSITIVE_SEARCH_TERMS.forEach(term => searchContent(term, description, descriptionBasis, true));
        NEGATIVE_SEARCH_TERMS.forEach(term => searchContent(term, description, descriptionBasis, false));
        // Search reviews
        reviews.forEach(review => {
            POSITIVE_SEARCH_TERMS.forEach(term => searchContent(term, review, cleanBasis, true));
            NEGATIVE_SEARCH_TERMS.forEach(term => searchContent(term, review, notCleanBasis, false));
        });
        // 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
        updateVerdict();
    }

    // Search text for a given term, add found position to given container and increment positive/negative
    function searchContent(term, content, container, positive) {
        let contentMatch = content.toLowerCase().match(term);
        if (contentMatch) {
            positive ? positives++ : negatives++;
            console.log(contentMatch);
            container.innerHTML += `
                <div class="contentComment">
                    ...${content.slice(contentMatch.index - 50, contentMatch.index)}<b class="content${positive ? '' : 'Not'}Clean">${content.substr(contentMatch.index, contentMatch[0].length)}</b>${content.slice(contentMatch.index + contentMatch[0].length, contentMatch.index + 50)}...
                </div>`;
        }
    }

    function updateVerdict() {
        let verdict = document.getElementById('crVerdict');
        if (positives && positives > negatives) {
            verdict.innerText = `${negatives ? 'Probably' : 'Most likely'} clean`;
            verdict.className += 'contentClean';
        } else if (negatives && negatives > positives) {
            verdict.innerText = `${positives ? 'Probably' : 'Most likely'} not clean`;
            verdict.className += 'contentNotClean';
        } else {
            verdict.innerText = positives && negatives ? 'Could be clean or not clean' : 'Unknown';
            verdict.className += 'contentUnknown';
        }
        document.getElementById('crPositives').innerText = positives;
        document.getElementById('crNegatives').innerText = negatives;
    }

    function expandDetails() {
        let collapsedText = '(Details)',
            expandedText = '(Hide)';
        if (this.innerText == collapsedText) {
            crDetails.style.display = 'block';
            this.innerText = expandedText;
        } else if (this.innerText == expandedText) {
            crDetails.style.display = 'none';
            this.innerText = collapsedText;
        }
    }

    start();
})();