Greasy Fork

Greasy Fork is available in English.

TART for amazon.de

Listet alle deine Rezensionen auf und informiert dich über Änderungen (neue Hilfreich-/Nicht hilfreich-Klicks, neue Kommentare)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/*===========================================================================*\
|  TART for amazon.de                                                         |
|  Based on The Amazon Review Tabulator - TART  v1.5.5                        |
|      (c) 2016-17 by Another Floyd                                           |
|  German fork by Strg-Alt-Entf                                               |
|  Ausgehend von "Mein Profil - Rezensionen" auf Amazon, listet es alle       |
|  deine Rezensionen auf und informiert dich über Änderungen                  |
|  (neue Hilfreich-/Nicht hilfreich-Klicks, neue Kommentare). Klicke auf      |
|  "Tabelle", um eine Übersicht über alle Rezensionen aufzurufen.             |
|  Klicke auf "Optionen" für Optionen.                                        |
\*===========================================================================*/

// ==UserScript==
// @name           TART for amazon.de
// @namespace      Strg-Alt-Entf.scripts
// @version        1.0.3
// @author         Strg-Alt-Entf
// @description    Listet alle deine Rezensionen auf und informiert dich über Änderungen (neue Hilfreich-/Nicht hilfreich-Klicks, neue Kommentare)
// @include        https://*amazon.de/gp/cdp/member-reviews*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_xmlhttpRequest
// @grant          GM_log
// @grant          GM_openInTab
// @grant          GM_info
// @require        https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @require        http://greasyfork.icu/scripts/20744-sortable/code/sortable.js?version=132520
// @require        https://openuserjs.org/src/libs/sizzle/GM_config.js
// @icon           data:image/png;base64,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
// ==/UserScript==

// Start

(function() {

    var showUpdatesOnly = false;
    var primaryDisplayBuffer = "";
    var updateDisplayBuffer = "";
    var oldTARTstats = [];

    var userID = "";
    var reviewCount = 0;
    var reviewerRanking = ""; //wird nicht mehr genutzt, aber beibehalten, falls Amazon die Seite mal wieder ändert
    var helpfulVotes = 0;

    var oldStoreItemIDs = [];
    var oldStoreUpvotes = [];
    var oldStoreDownvotes = [];
    var oldStoreComments = [];

    var newStoreItemIDs = "";
    var newStoreUpvotes = "";
    var newStoreDownvotes = "";
    var newStoreComments = "";

    var tallyWordcount = 0;
    var tallyUpvotes = 0;
    var tallyDownvotes = 0;
    var tallyAllvotes = 0;
    var tallyStars = 0;
    var tallyComments = 0;
    var tallyAVP = 0;
    var tallyVine = 0;

    // use this reference for progress indicator
    var profileDiv = "";
    var profileDivOriginalHTML = "";
    var profileDivTabulateHTML = "<br></br><a href='javascript:tabulate();'>Tabelle</a> <a href='javascript:options();' title='Klicke für TART Optionen' >Optionen</a>";
    
    function assembleDisplayBuffers (completeSetOfTableRows, reviewsProcessed) {

        var today = new Date();
        var formattedToday = today.toLocaleDateString('de-DE',{month:'long',day:'numeric',year:'numeric'});
        var toggleLink = (GM_config.get('DisplayMode')) ? "<p><a href='javascript:toggleView();'>Ansicht umschalten: Alle Rezensionen / Nur Änderungen</a>" : "";
        var bMargin = (GM_config.get('FixedFooter')) ? "36" : "0";
        var upvoteReviewRatio = (helpfulVotes/reviewCount).toFixed(2);

        // set up top of display page
        primaryDisplayBuffer = "<!DOCTYPE html><html lang='de'>" +
            "<head><meta charset='utf-8'/><title>TART Amazon Rezensionsübersicht</title>" +
            "<style type='text/css'>" +
            "body {font-family:Arial,sans-serif;font-size:" + GM_config.get('FontSize') + "px; margin:0; padding:0px 5px}" +
            ".tg  {border-collapse:collapse;border-spacing:0;width:100%}" +
            ".tg td{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px; text-align:right}" +
            ".tg th{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px; text-align:right; font-weight:bold; background-color:#010066; color:#ffffff}" +
            ".tg .cell-left{text-align:left}" +
            ".tg .hilite-left{text-align:left;background-color:#" + GM_config.get('HighliteColor') + "}" +
            ".tg .hilite-right{background-color:#" + GM_config.get('HighliteColor') + "}" +
            "#tblMain.hide7 tr td:nth-child(7), #tblMain.hide7 tr th:nth-child(7) {display: none}" +
            "#tblMain.hide10 tr td:nth-child(10), #tblMain.hide10 tr th:nth-child(10) {display: none}" +
            "#tblMain.hide11 tr td:nth-child(11), #tblMain.hide11 tr th:nth-child(11) {display: none}" +
            "#footer {position:fixed; bottom:0}" +
            "#footer.hide7 tr td:nth-child(7), #footer.hide7 tr th:nth-child(7) {display: none}" +
            "#footer.hide10 tr td:nth-child(10), #footer.hide10 tr th:nth-child(10) {display: none}" +
            "#footer.hide11 tr td:nth-child(11), #footer.hide11 tr th:nth-child(11) {display: none}" +
            ".txtLarge {font-size:18px;font-weight:bold}" +
            ".summaryLink, .summaryLink:link, .summaryLink:visited {text-decoration:none; font-weight:bold; color:#000000}" +
            ".tableLink, .tableLink:link, .tableLink:visited {text-decoration:none; font-weight:bold; font-size:110%; color:#000000}" +
            ".footerLink, .footerLink:link, .footerLink:visited {text-decoration:none; font-weight:bold; color:#FFFF12}" +
            "table.sortable th.sorted {background-color:#000000}" +
            "</style></head><body>" +
            "<span class='txtLarge'>Amazon Rezensionsübersicht</span><br>" +
            "Erstellt mit <a href='http://greasyfork.icu/de/scripts/31289-tart-for-amazon-de' target='_new'>TART for amazon.de " + GM_info.script.version + "</a> - " + formattedToday +
            "<p>Kundenrezensionen: " + checkChange(reviewCount, oldTARTstats[6], false) + "<br>" +
            "Hilfreich-Klicks: " + checkChange(helpfulVotes, oldTARTstats[7], false) + "<br>" +
            "Hilfreich-Klicks pro Rezension: " + checkChange(upvoteReviewRatio, oldTARTstats[8], false) + toggleLink +
            "</p><table class='tg sortable' id='tblMain' style='margin-bottom:" + bMargin + "px'>" +
            "<thead><tr>" +
            "<th class='cell-left sort-number sort-default' style='width:6%'>#</th>" +
            "<th class='cell-left sort-text' style='width:33%'>Produkt</th>" +
            "<th class='cell-left sort-date' style='width:12%'>Datum</th>" +
            "<th class='sort-number'>Sterne</th>" +
            "<th class='sort-number'>Hilfreich</th>" +
            "<th class='sort-number'>Nicht hilfreich</th>" +
            "<th class='sort-number'>Gesamt</th>" +
            "<th class='sort-number'>%&nbsp;Hilfreich</th>" +
            "<th class='sort-number'>Kommentare</th>" +
            "<th class='sort-text' style='width:6%'>Verifiziert</th>" +
            "<th class='sort-text' style='width:6%'>Vine</th>" +
            "</tr></thead><tbody>";

        // Column widths above are assigned to columns that have heading shorter than
        // data is likely to be; widths are duplicated at separate footer table, to keep
        // them all in sync

        updateDisplayBuffer = primaryDisplayBuffer; // both displays have same top section
        primaryDisplayBuffer += completeSetOfTableRows;

        // info needed in footer
        var calcStars = (tallyStars/reviewsProcessed).toFixed(1);
        var calcHelpfulPct = helpfulPercent(tallyUpvotes,tallyDownvotes);
        var avgWordsPerReview = (tallyWordcount/reviewsProcessed).toFixed(0);
        var newTARTstats = calcStars + " " + tallyUpvotes + " " + tallyDownvotes + " " + calcHelpfulPct + " " + tallyComments + " " + reviewerRanking + " " + reviewCount + " " + helpfulVotes + " " + upvoteReviewRatio + " " + tallyAllvotes + " " + tallyAVP + " " + tallyVine + " " + reviewsProcessed + " " + avgWordsPerReview;
        GM_setValue("recentFooterValues", newTARTstats.trim()); // write 'em with new values

        var visibleFooterRow = "<tr>" +
            "<th style='text-align:left'>" + checkChange(reviewsProcessed, oldTARTstats[12], true) + "</th>" +
            "<th style='text-align:left'>Durchschnittliche Wortzahl pro Rezension: " + checkChange(avgWordsPerReview, oldTARTstats[13], true) + "</th>" +
            "<th></th>" +
            "<th>" + checkChange(calcStars, oldTARTstats[0], true) + "</th>" +
            "<th>" + checkChange(tallyUpvotes, oldTARTstats[1], true) + "</th>" +
            "<th>" + checkChange(tallyDownvotes, oldTARTstats[2], true) + "</th>" +
            "<th>" + checkChange(tallyAllvotes, oldTARTstats[9], true) + "</th>" +
            "<th>" + checkChange(calcHelpfulPct, oldTARTstats[3], true) + "</th>" +
            "<th>" + checkChange(tallyComments, oldTARTstats[4], true) + "</th>" +
            "<th>" + checkChange(tallyAVP, oldTARTstats[10], true) + "</th>" +
            "<th>" + checkChange(tallyVine, oldTARTstats[11], true) + "</th>" +
            "</tr>";

        // add footer either to be fixed at bottom of screen, or normal
        if(GM_config.get('FixedFooter')) {
            var hiddenRowForColumnWidths = "<tr style='visibility:hidden'>" +
            "<th style='width:6%; border-style: hidden'></th>" +
            "<th style='width:33%; border-style: hidden'></th>" +
            "<th style='width:12%; border-style: hidden'></th>" +
            "<th style='border-style: hidden'>Sterne</th>" +
            "<th style='border-style: hidden'>Hilfreich</th>" +
            "<th style='border-style: hidden'>Nicht Hilfreich</th>" +
            "<th style='border-style: hidden'>Gesamt</th>" +
            "<th style='border-style: hidden'>%&nbsp;Hilfreich</th>" +
            "<th style='border-style: hidden'>Kommentare</th>" +
            "<th style='width:6%; border-style: hidden'></th>" +
            "<th style='width:6%; border-style: hidden'></th></tr>";

            // create detached table for footer
            // fixed, by virtue of the styled 'footer' id
            primaryDisplayBuffer += "</tbody></table><table class='tg' id='footer' style='width:calc(100% - 10px)'><tfoot>" + hiddenRowForColumnWidths + visibleFooterRow + "</tfoot></table></body></html>"; 
        }
        else {
            primaryDisplayBuffer += "</tbody><tfoot>" + visibleFooterRow + "</tfoot></table></body></html>"; // normal
        }

        // get rows containing updated reviews, only
        var tempDiv = document.createElement('div');
        tempDiv.innerHTML = primaryDisplayBuffer;

        var findUpdateRows = document.evaluate("//td[@class='hilite-left']/..", tempDiv, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        for(var d = 0; d < findUpdateRows.snapshotLength; d++) {
            updateDisplayBuffer += findUpdateRows.snapshotItem(d).outerHTML;
        }
        updateDisplayBuffer += "</tbody></table></body></html>";
    }

    function tabulate() {

        // reset global accumulators to ensure that repeated script runs
        // (non-enhanced mode) remain clean
        newStoreItemIDs = "";
        newStoreUpvotes = "";
        newStoreDownvotes = "";
        newStoreComments = "";

        tallyWordcount = 0;
        tallyUpvotes = 0;
        tallyDownvotes = 0;
        tallyAllvotes = 0;
        tallyStars = 0;
        tallyComments = 0;
        tallyAVP = 0;
        tallyVine = 0;

        // read in stored info from past run, for use in change detection
        oldStoreItemIDs = GM_getValue("recentItemIDs", "").split(" ");
        oldStoreUpvotes = GM_getValue("recentUpvotes", "").split(" ");
        oldStoreDownvotes = GM_getValue("recentDownvotes", "").split(" ");
        oldStoreComments = GM_getValue("recentComments", "").split(" ");

        // prepare url with country domain and user ID, ready for review page number
        var tld = "de";
        var url = window.location.href;
        var urlStart = "https://www.amazon." + tld + "/gp/cdp/member-reviews/" + userID + "?ie=UTF8&display=public&page=";
        var urlEnd = "&sort_by=MostRecentReview";

        // space and counters for incoming data
        var perPageResponseDiv = [];
        var pageSetOfTableRows = [];
        var pageResponseCount = 0;
        var reviewsProcessed = 0;
        var pageCount = Math.floor(reviewCount / 10) + ((reviewCount % 10 > 0) ? 1 : 0);
        //var pageCount = 3; // for testing

        // initialize the progress indicator
        // sort of pre-redundant to do this here AND in the loop, but,
        // looks better, if there is a lag before the first response
        var progressHTML = "<br></br><b>" + pageCount + "</b>";
        profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;

        // download and process Amazon pages
        var receivedPageWithNoReviews = false;
        var x = 1;
        while (x <= pageCount) {
            (function(x){
                var urlComplete = urlStart + x + urlEnd;
                perPageResponseDiv[x] = document.createElement('div');

                GM_xmlhttpRequest({
                    method: "GET",
                    url: urlComplete,
                    onload: function(response) {

                        // capture incoming data
                        perPageResponseDiv[x].innerHTML = response.responseText;
                        pageResponseCount++;

                        // update the progress indicator
                        var progressHTML = "<br></br><b>" + (pageCount - pageResponseCount) + "</b>";
                        profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;

                        // get parent of any reviewText DIV
                        var findReviews = document.evaluate("//div[@class='reviewText']/..", perPageResponseDiv[x], null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); // evaluating the doc DIV made above
                        var reviewsOnPage = findReviews.snapshotLength;
                        if(reviewsOnPage == 0) receivedPageWithNoReviews = true;

                        // process each review found on current page
                        pageSetOfTableRows[x] = ""; // initialize each member prior to concatenating

                        for (var j = 0; j < reviewsOnPage; j++) {

                            var oneReview = findReviews.snapshotItem(j);
                            var reviewChildren = oneReview.children;
                            var childCount = reviewChildren.length;

                            var commentCount = 0;
                            var itemTitle = "Produkt nicht verfügbar";
                            var itemLink = "";
                            var permaLink = "";
                            var starRating = 0;
                            var reviewDate = "";
                            var upVotes = 0;
                            var downVotes = 0;
                            var totalVotes = 0;
                            var itemID = "";
                            var isAVP = 0;
                            var isVine = 0;

                            // get number of comments, and permalink
                            var tempText = reviewChildren[childCount-2].textContent;
                            if(tempText.indexOf('Kommentar (') > -1 || tempText.indexOf('Kommentare (') > -1) {
                                var paren1 = tempText.indexOf('(');
                                var paren2 = tempText.indexOf(')');
                                commentCount = tempText.substring(paren1+1,paren2);
                                commentCount = parseInt(commentCount.replace(/\./g, '')); // entferne Tausenderpunkte
                            }
                            var lst = reviewChildren[childCount-2].getElementsByTagName('a');
                            permaLink = lst[2].getAttribute("href");

                            // get review wordcount and add to tally
                            tempText = reviewChildren[childCount-3].textContent;
                            tallyWordcount += countWords(tempText);

                            // the data items below do not have reliable positions, due to presence
                            // or not, of vine voice tags, verified purchase, votes, etc.
                            // so, are done in a loop with IF checks. Must start loop just above review
                            // text, in case the reviewer has used any of the phrases I am searching for
                            for (var i = childCount - 4; i > -1; i--) {

                                var childHTML = reviewChildren[i].innerHTML;

                                // get item title and item link
                                var titleClue = childHTML.indexOf('Rezension bezieht sich auf');
                                if(titleClue > -1) {
                                    var lst = reviewChildren[i].getElementsByTagName('a');
                                    itemLink = lst[0].getAttribute("href");
                                    itemTitle = lst[0].textContent;
                                }

                                // get star rating AND review date
                                var ratingClue = childHTML.indexOf('von 5 Sternen');
                                if(ratingClue > -1) {
                                    starRating = childHTML.substring(ratingClue-4,ratingClue-1);
                                    reviewDate = reviewChildren[i].lastElementChild.textContent;
                                    var lst = reviewDate.split(" ");
                                    reviewDate = lst[0].substring(0,3) + " " + lst[1] + " " + lst[2];
                                }

                                // get vote counts
                                var childText = reviewChildren[i].textContent;
                                var voteClue = childText.indexOf('Kunden fanden die folgende Rezension hilfreich');
                                if(voteClue > -1) {
                                    var list = childText.trim().split(" "); // there were extra, invisible spaces!
                                    upVotes = parseInt(list[0].replace(/\./g, '')); // entferne Tausenderpunkte
                                    totalVotes = parseInt(list[2].replace(/\./g, ''));
                                    downVotes = totalVotes - upVotes;
                                }

                                // check for AVP and Vine
                                var avpClue = childHTML.indexOf('Verifizierter Kauf');
                                if(avpClue > -1) isAVP = 1;

                                var vineClue = childHTML.indexOf('Vine Kundenrezension eines kostenfreien Produkts');
                                if(vineClue > -1) isVine = 1;
                            }

                            // get item ID
                            var lst = oneReview.parentNode.getElementsByTagName('a');
                            itemID = lst[0].getAttribute("name");

                            // get HTML formatted table row; rows COULD be accumulated in
                            // preOneTableRow; but, since they come in page sets that may be
                            // received out of order, the non-enhanced view (which has no sort,
                            // thus no default sort) would appear out of order
                            pageSetOfTableRows[x] += prepOneTableRow((j+1+(x-1)*10),itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount,totalVotes,isAVP,isVine);

                            reviewsProcessed++; // more reliable than reviewCount, for calculating avg. rating
                        }

                        // clear the response, to save memory --
                        // could be critical when there are many review pages
                        perPageResponseDiv[x].innerHTML = "";

                        // see if all data from multiple page loads has arrived
                        if(pageResponseCount==pageCount) {

                            // assemble the sets of table rows, which will be in proper order
                            // rather than order received
                            var completeSetOfTableRows = "";
                            for(var y=1; y <= pageCount; y++) {
                                completeSetOfTableRows += pageSetOfTableRows[y];
                            }

                            assembleDisplayBuffers(completeSetOfTableRows, reviewsProcessed);

                            // store info to be used in subsequent run, for change detection
                            GM_setValue("recentItemIDs", newStoreItemIDs.trim());
                            GM_setValue("recentUpvotes", newStoreUpvotes.trim());
                            GM_setValue("recentDownvotes", newStoreDownvotes.trim());
                            GM_setValue("recentComments", newStoreComments.trim());

                            // replace progress indicator with Tabulate link
                            profileDiv.innerHTML = profileDivOriginalHTML + profileDivTabulateHTML;

                            // show message if any of the received pages contained NO reviews...
                            // SOMETHING was received -- an empty, error, or 'please try again' type page
                            if(receivedPageWithNoReviews) {
                                alert("Eine oder mehr Rezensionenseiten wurden nicht empfangen. \n\nHervorgehobene Änderungen zu Rezensionen stimmen und werden beim nächsten Lauf nicht mehr hervorgehoben. \n\nAlle fehlenden Rezensionen werden im nächsten lauf als neu hervorgehoben.");
                            }

                            // --- display the results
                            if(!GM_config.get('DisplayMode')) GM_openInTab("data:text/html," + encodeURIComponent(primaryDisplayBuffer));
                            else {
                                document.body.innerHTML = primaryDisplayBuffer;
                                manageColumns();
                            }
                        }
                    }
                });
            })(x);
            x++;
        }
    }

    function manageColumns() {
        if(!GM_config.get('ShowAllVotes')) {
            document.getElementById("tblMain").classList.toggle("hide7");
            if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide7");
        }

        if(!GM_config.get('ShowAVP')) {
            document.getElementById("tblMain").classList.toggle("hide10");
            if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide10");
        }

        if(!GM_config.get('ShowVine')) {
            document.getElementById("tblMain").classList.toggle("hide11");
            if(!showUpdatesOnly) document.getElementById("footer").classList.toggle("hide11");
        }
    }

    function countWords(s){ // from 'neokio' on StackOverflow
        s = s.replace(/\n/g,' '); // newlines to space
        s = s.replace(/(^\s*)|(\s*$)/gi,''); // remove spaces from start + end
        s = s.replace(/[ ]{2,}/gi,' '); // 2 or more spaces to 1
        return s.split(' ').length;
    }

    function invalidValue(oldStoredValue) {
        if(oldStoredValue === undefined || oldStoredValue == "?") return true;
        return false;
    }

    function checkChange(newStat,oldStat,forFooter) {
        if(newStat == oldStat || invalidValue(oldStat) === true) return newStat;
        else {
            var linkClass = "summaryLink";
            if(forFooter) linkClass = "footerLink";
            return "<a href='javascript: void(0)' class='" + linkClass + "' title='Zuvor: " + oldStat + "'>" + newStat + "</a>";
        }
    }

    function toggleView() {
        showUpdatesOnly = !showUpdatesOnly;
        if(showUpdatesOnly) document.body.innerHTML = updateDisplayBuffer;
        else document.body.innerHTML = primaryDisplayBuffer;
        manageColumns();
    }

    function helpfulPercent(upVotes,downVotes) {
        var helpfulPercent = "";
        upVotes = upVotes;
        downVotes = downVotes;
        if(upVotes + downVotes > 0) helpfulPercent = (upVotes/(upVotes+downVotes)*100).toFixed(1);

        return helpfulPercent;
    }

    function prepOneTableRow (row,itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount,totalVotes,isAVP,isVine) {

        // do these before mangling the values with <b> tags </b>
        var helpfulPct = helpfulPercent(upVotes,downVotes);
        itemTitle = "<a href='" + permaLink + "' target='_new'>" + itemTitle.substring(0,40) + "</a>";

        // keep tallies to use in table footer
        tallyUpvotes += upVotes;
        tallyDownvotes += downVotes;
        tallyAllvotes += totalVotes;
        tallyStars += parseInt(starRating);
        tallyComments += commentCount;
        tallyAVP += isAVP;
        tallyVine += isVine;

        // assemble storage info, to use in subsequent run, for change detection
        newStoreItemIDs += itemID + " ";
        newStoreUpvotes += upVotes + " ";
        newStoreDownvotes += downVotes + " ";
        newStoreComments += commentCount + " ";

        // see if review for this item has previously been examined
        var matchIdx = -1;
        for(var i=0; i<oldStoreItemIDs.length; i++) {
            if(oldStoreItemIDs[i] == itemID) {
                // we have a match, an item that has previously been seen
                matchIdx = i;
                break;
            }
        }

        var hiliteRow = false;
        if(matchIdx > -1) {
            // entry exists; see if any of the numbers have changed
            if(oldStoreUpvotes[matchIdx] != upVotes) {
                // for changed number, make it bold, and hilite row
                // and store previous value for display as tooltip, for mouse hover
                upVotes = "<a href='javascript: void(0)' class='tableLink' title='Zuvor: " + oldStoreUpvotes[matchIdx] + "'>" + upVotes + "</a>";
                hiliteRow = true;
            }
            if(oldStoreDownvotes[matchIdx] != downVotes) {
                downVotes = "<a href='javascript: void(0)' class='tableLink' title='Zuvor: " + oldStoreDownvotes[matchIdx] + "'>" + downVotes + "</a>";
                hiliteRow = true;
            }
            if(oldStoreComments[matchIdx] != commentCount) {
                commentCount = "<a href='javascript: void(0)' class='tableLink' title='Zuvor: " + oldStoreComments[matchIdx] + "'>" + commentCount + "</a>";
                hiliteRow = true;
            }
        }
        else {
            // no match, so, it's a new review; bold the title and hilite the row
            itemTitle = "<b>" + itemTitle + "</b>";
            hiliteRow = true;
        }

        var tdLeft = "<td class='cell-left'>";
        var tdRight = "<td>";
        if(hiliteRow===true && oldStoreItemIDs[0].length > 0) {
            tdLeft = "<td class='hilite-left'>";
            tdRight = "<td class='hilite-right'>";
        }

        var tableRow = "<tr>" + tdLeft + row + "</td>" + tdLeft + itemTitle + "</td>" + tdLeft + reviewDate + "</td>" + tdRight + starRating + "</td>" + tdRight + upVotes + "</td>" + tdRight + downVotes + "</td>" + tdRight + totalVotes + "</td>" + tdRight + helpfulPct + "</td>" + tdRight +commentCount + "</td>" + tdRight + ((isAVP > 0) ? "&bull;" : "") + "</td>" + tdRight + ((isVine > 0) ? "&bull;" : "") + "</td></tr>";

        return tableRow;
    }

    // create Options menu with GM_config

    var frame = document.createElement('div');
    document.body.appendChild(frame);
    GM_config.init(
    {
    'id': 'MyConfig', // The id used for this instance of GM_config
    'title': 'TART Optionen', // Panel Title

    'fields': // Fields object
        {
        'DisplayMode': // Line item
            {
            'type': 'checkbox',
            'label': 'Erweiterte Ansicht (Abwählen für neuen Tabulator mit weniger Optionen)',
            'default': true
            },

        'FixedFooter':
            {
            'type': 'checkbox',
            'label': 'Zeige fixierte Zusammenfassung am Ende der Seite',
            'default': true
            },

        'ShowAllVotes':
            {
            'type': 'checkbox',
            'label': 'Zeige Alle-Klicks-Spalte',
            'default': true
            },

        'ShowAVP':
            {
            'type': 'checkbox',
            'label': 'Zeige Verifizierter-Kauf-Spalte',
            'default': true
            },

        'ShowVine':
            {
            'type': 'checkbox',
            'label': 'Zeige Vine-Spalte',
            'default': true
            },

        'FontSize':
            {
            'label': 'Textgröße',
            'type': 'unsigned int',
            'size': 2,
            'default': 12
            },

        'RowPadding':
            {
            'label': 'Zeilenhöhe',
            'type': 'unsigned int',
            'size': 2,
            'default': 10
            },

        'HighliteColor':
            {
            'label': 'Hervorhebungsfarbe (6-stelliger Hex-Code)',
            'title': 'From graphics program or online color picker',
            'type': 'text',
            'size': 6,
            'default': 'FFFF55'
            }
        },

    'events': // Callback functions object
        {
        'open': function() {
            // style the panel as it's being displayed
            frame.style.position = "auto";
            frame.style.width = "auto";
            frame.style.height = "auto";
            frame.style.backgroundColor = "#F3F3F3";
            frame.style.padding = "10px";
            frame.style.borderWidth = "5px";
            frame.style.borderStyle = "ridge";
            frame.style.borderColor = "gray";
            var x = (document.documentElement.clientWidth - frame.offsetWidth) / 2;
            frame.style.left = x + 'px';
            }
        },

    'frame': frame, // specify the DIV element used for the panel

    'css': '#MyConfig .config_header { font-size: 12pt; font-weight:bold; margin-bottom:12px }' +
           '#MyConfig .field_label { font-size: 12px; font-weight:normal; margin: 0 3px }'
    });

    // event listener to pick up mouse clicks, to run script functions

    document.addEventListener('click', function(event) {
        var tempstr = new String(event.target);
        var quash = false;

        if(tempstr.indexOf('tabulate') > -1) {
            tabulate();
            quash = true;
            }

        if(tempstr.indexOf('options') > -1) {
            GM_config.open();
            quash = true;
        }

        if(tempstr.indexOf('toggleView') > -1) {
            toggleView();
            quash = true;
        }

        if(quash) {
            event.stopPropagation();
            event.preventDefault();
        }
    }, true);

    // initiate the script

    function main() {

        var findProfileLink = "";
        var url = window.location.href;
        // read previous values for footer and top summary values
        oldTARTstats = GM_getValue("recentFooterValues", "? ? ? ? ? ? ? ? ? ? ? ? ? ?").split(" ");

        // Profil-Link suchen
        findProfileLink = document.evaluate("//a[contains(.,'Mein Profil')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        // User-ID extrahieren
        GM_log("Finding account info...");
        var profileLink = findProfileLink.snapshotItem(0).getAttribute("href");
        var lst = profileLink.split("/");
        userID = lst[4];
        GM_log("User ID: " + userID);

        // find profile info panel
        var findDiv = document.evaluate("//div[contains(.,'Hilfreiche Bewertungen')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        profileDiv = findDiv.snapshotItem(0);

        // hole die Hilfreich-Klicks
        lst = profileDiv.textContent.split(" ");
        helpfulVotes = lst[3].substring(12);
        GM_log("Helpful Votes: " + helpfulVotes);

        // get review count
        var prevSibDiv = profileDiv.previousElementSibling;
        charIdx = prevSibDiv.textContent.lastIndexOf(':');
        reviewCount = prevSibDiv.textContent.substring(charIdx+2);
        // eventuelle Tausenderpunkte entfernen
        reviewCount = parseInt(reviewCount.replace(/\./g, ''));
        GM_log("Review Count: " + reviewCount);

        // add Tabulate link; also, save content for use with progress indicator
        profileDivOriginalHTML = profileDiv.innerHTML;
        profileDiv.innerHTML += profileDivTabulateHTML;

        // add delta symbol with mouseover note, if there are obvious new values to Tabulate
        // but, don't show delta on first run, which would have invalid comparison values
        if((helpfulVotes != oldTARTstats[7] && invalidValue(oldTARTstats[7]) === false)) {
            profileDiv.innerHTML += " <a href='javascript: void(0)' style='text-decoration:none; color:#000000' title='Zahl der Rezensionen und/oder Bewertungen haben sich seit der letzten Analyse geändert'>&#916;</a>";
        }
    }

    main();

})();
// End