There are 8 previous versions of this script.
// ==UserScript==
// @name US.o script author helper
// @namespace http://userscripts.org/scripts/show/36353
// @description Shows how many new comments, installs and fans your scripts have got since you last checked
// @include http://userscripts.org/home/scripts
// @include http://userscripts.org/home/scripts?*
// ==/UserScript==
// Reverses a string
String.prototype.reverse = function() {
return this.split("").reverse().join("");
};
// Transforms a number into a string with a coma as thousands separator
// If showSign is true, the plus (+) sign is appended to the positive numbers
// The double reverse is to workaround Javascript lack of lookbehind in regular expressions
Number.prototype.toCustomString = function(showSign) {
var strNumber = this.toString();
strNumber = strNumber.reverse().replace(/\d{3}(?!,|$|-)/g, "$&,").reverse();
if (showSign) strNumber = ((this > 0) ? "+" : "") + strNumber;
return strNumber;
};
// Runs a particular XPath expression p against the context node context (or the document, if not provided)
// Returns the results as an array
function $x(p, context) {
if (!context) context = document;
var arr = [], xpr = document.evaluate(p, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, l = xpr.snapshotLength; i < l; i++) arr.push(xpr.snapshotItem(i));
return arr;
}
// Returns only the first element of the array returned by $x (or null if the array was empty)
function $x1(p, context, docObj) {
var nodeArray = $x(p, context, docObj);
return (nodeArray.length > 0) ? nodeArray[0] : null;
}
// Creates a new node with the given attributes and properties (be careful with XPCNativeWrapper limitations)
function createNode(type, attributes, props) {
var node = document.createElement(type);
if (attributes) {
for (var attr in attributes) {
node.setAttribute(attr, attributes[attr]);
}
}
if (props) {
for (var prop in props) {
if (prop in node) node[prop] = props[prop];
}
}
return node;
}
// Returns the "pluralized" form of singularWord if it's necessary to express a timesNumber quantity
function adjustPlural(singularWord, timesNumber) {
return singularWord + ((Math.abs(timesNumber) != 1) ? "s" : "");
}
// Creates a span with the difference number as its text and a CSS class according to its sign
// If formatString is provided, the span will present it as its text (with each %s replaced with the difference number)
function createDiffSpan(diffNumber, formatString) {
if (!formatString) formatString = " (%s)"; // Default span format
return createNode("span", {class: ((diffNumber > 0) ? "gsPosDiff" : "gsNegDiff")},
{textContent: formatString.replace(/%s/g, diffNumber.toCustomString(true))});
}
// Calculates the difference between two dates and returns it as a "humanized" string
// It uses UTC functions because the timezone offset can affect the calculations
function getDateDiffString(dateNew, dateOld) {
// Creates a date object with the difference between the two passed dates
var dateDiff = new Date(dateNew.getTime() - dateOld.getTime());
dateDiff.setUTCFullYear(dateDiff.getUTCFullYear() - 1970); // Substracts 1970 years to compensate Date.getTime's (Unix) epoch (1 Jan 1970 00:00:00 UTC)
// Initializes the variables (timeunitsHash correlates the time units names with its corresponding method of the Date object)
var strDateDiff = "", timeunitValue = 0;
var timeunitsHash = {year: "getUTCFullYear", month: "getUTCMonth", day: "getUTCDate",
hour: "getUTCHours", minute: "getUTCMinutes", second: "getUTCSeconds", millisecond: "getUTCMilliseconds"};
// Appends the time units values and its names to construct the string
for (var timeunitName in timeunitsHash) {
// Gets the time unit value by calling the corresponding method of the Date object
// It substracts 1 for the days because they begin with 1 (the other time units begin with 0)
timeunitValue = dateDiff[timeunitsHash[timeunitName]]() - ((timeunitName == "day") ? 1 : 0);
// If the value isn't 0, appends this time unit to the string
if (timeunitValue !== 0) {
if ((timeunitName == "millisecond") && (strDateDiff.length !== 0)) continue; // Milliseconds won't be added unless the difference is less than 1 second
strDateDiff += ((strDateDiff.length === 0) ? "" : ", ") + // Adds a comma as separator if another time unit has already been added
timeunitValue.toCustomString(false) + " " +
adjustPlural(timeunitName, timeunitValue);
}
}
// Replaces the last comma with an "and" to humanize the string
strDateDiff = strDateDiff.replace(/,([^,]*)$/, " and$1");
return strDateDiff;
}
// Class for a base object used in several properties and methods of the GmScript and GmScriptContainer classes
function GmBase(baseComments, baseFans, baseInstalls) {
this.comments = baseComments;
this.fans = baseFans;
this.installs = baseInstalls;
}
// Class for the script object
// Each script row will be parsed to an object of this class and added to a SOA
function GmScript(scriptId, scriptName, scriptComments, scriptFans, scriptInstalls) {
this.id = scriptId; // Script id
this.name = scriptName; //Script name
this.props = new GmBase(scriptComments, scriptFans, scriptInstalls); // Script's properties object
}
// Class for the scripts' container object
function GmScriptContainer(dataKeyName) {
// Script objects array (SOA). The data will be retrieved from about:config
this.scripts = this.getScriptsData(dataKeyName);
// Totals object. Its properties are calculated according to the SOA
this.totals = {_parent: this, // Private variable that holds a reference to the GmScriptContainer object for later use (because, when the getters or _sum are called, "this" will refer to the totals object)
_sum: function(propName) { // Private function that calculates the sum of "propName" in the scripts of the SOA
var totalItems = 0;
this._parent.scripts.forEach(function(s) {totalItems += s.props[propName];});
return totalItems;
},
get comments() {return this._sum("comments");}, // Comments getter
get fans() {return this._sum("fans");}, // Fans getter
get installs() {return this._sum("installs");}}; // Installs getter
}
// Returns the script object from the container's SOA with the passed id
// If no script is found, it returns null
GmScriptContainer.prototype.getScriptById = function(scriptId) {
var scriptArrayFiltered = this.scripts.filter(function(s) {return (s.id == scriptId);});
return (scriptArrayFiltered.length > 0) ? scriptArrayFiltered[0]: null;
};
// Calculates the differences between the totals of this container and the ones of containerOld
// Returns the results in a GmBase object
GmScriptContainer.prototype.calculateDiffs = function(containerOld) {
var retObj = new GmBase(0, 0, 0);
for (var propName in retObj) retObj[propName] = this.totals[propName] - containerOld.totals[propName];
return retObj;
};
// Saves the SOA to about:config in JSON format
GmScriptContainer.prototype.saveScriptsData = function(dataKeyName) {
GM_setValue(dataKeyName, uneval(this.scripts));
};
// Returns the SOA stored in about:config, converting it from JSON format
// If dataKeyName isn't passed, or it or its content evaluates to false, an empty array is returned instead
GmScriptContainer.prototype.getScriptsData = function(dataKeyName) {
if (!dataKeyName) return [];
return (eval(GM_getValue(dataKeyName, "null")) || []);
};
// Adds classes to colorize positive and negative differences
GM_addStyle(".gsPosDiff {color: green !important}");
GM_addStyle("th > .gsPosDiff {color: lime !important}"); // For better contrast with the th black background
GM_addStyle(".gsNegDiff {color: red !important}");
// Associative array used to match each script property name with its corresponding column number (cells collection) in a script row
var columnsHash = {comments: 3, fans: 4, installs: 5};
// Gets the scripts table (tbody)
var scriptTable = $x1("id('content')//table/tbody");
if (!scriptTable) return;
// Gets an array with all the rows that correspond to scripts
var scriptRows = $x("./tr[starts-with(@id, 'scripts-')]", scriptTable);
if (scriptRows.length === 0) return;
// Parses the script rows into script objects (GmScript class) and adds them to the SOA of a GmScriptContainer object. The old SOA is loaded into a container as well
// Then it compares each script object from the SOA with the corresponding script object from the old SOA, presenting the differences in the document
var containerNew = new GmScriptContainer(), containerOld = new GmScriptContainer("scriptData");
scriptRows.forEach(function(scriptRow) {
// Finds the link to the script homepage
var scriptLink = $x1("./td[@class='script-meat']/a", scriptRow);
if (!scriptLink) return;
// Extracts the script name and id (parsed to an integer) from the link
var scriptName = scriptLink.textContent;
var scriptId = scriptLink.href.match(/\/(\d+)$/);
if (scriptId === null) {
return;
}
else {
scriptId = parseInt(scriptId[1], 10);
if (isNaN(scriptId)) return;
}
// Extracts and parses the comments, fans and installs numbers from the apropiate cell of the script row
var parseObj = new GmBase(0, 0, 0);
for (var propName in parseObj) {
var scriptPropCell = scriptRow.cells[columnsHash[propName]];
parseObj[propName] = (scriptPropCell) ? parseInt(scriptPropCell.textContent, 10) : NaN;
}
if (isNaN(parseObj.comments) || isNaN(parseObj.fans) || isNaN(parseObj.installs)) return;
// Creates a script object with the extracted data and adds it to the SOA
var scriptObj = new GmScript(scriptId, scriptName, parseObj.comments, parseObj.fans, parseObj.installs);
containerNew.scripts.push(scriptObj);
// If there isn't any saved data, no comparison is necessary
if (containerOld.scripts.length === 0) return;
// Gets the corresponding script object from the old SOA
var scriptObjOld = containerOld.getScriptById(scriptObj.id);
if (scriptObjOld === null) return;
// Compares each property (comments, fans and installs) of the script objects
for (var propName in scriptObj.props) {
// Calculates the difference between properties. If it's zero, there's nothing to indicate
var propDiff = scriptObj.props[propName] - scriptObjOld.props[propName];
if (propDiff === 0) continue;
// Creates a difference span with the difference number
var spanDiff = createDiffSpan(propDiff);
// Finds the appropiate cell for this property using the scriptRow object (row) and the columnsHash information (column)
var propCell = scriptRow.cells[columnsHash[propName]];
if (!propCell) continue;
// Appends the span to the cell
propCell.appendChild(spanDiff);
}
});
if (containerNew.scripts.length === 0) return; // If no script object has been added to the SOA, the script returns
// Saves the new SOA to about:config
containerNew.saveScriptsData("scriptData");
// Gets the headers' row of the script table
var scriptHeaderRow = $x1("./tr[th]", scriptTable);
if (!scriptHeaderRow) return;
// Calculates the differences between the totals of the new and old container, presenting the differences in the document
if (containerOld.scripts.length !== 0) {
var diffObj = containerNew.calculateDiffs(containerOld);
for (var propName in diffObj) {
if (diffObj[propName] !== 0) scriptHeaderRow.cells[columnsHash[propName]].appendChild(createDiffSpan(diffObj[propName]));
}
}
// Creates a paragraph with the totals and inserts it into the document before the script table
var totalsText = containerNew.scripts.length.toCustomString(false) + " " + adjustPlural("script", containerNew.scripts.length);
for (var propName in containerNew.totals) {
if (propName.charAt(0) != "_") { // Private members should be ignored
totalsText += " / " + containerNew.totals[propName].toCustomString(false) + " " + adjustPlural(propName.slice(0, -1), containerNew.totals[propName]);
}
}
var totalsP = createNode("p", {class: "subtitle"}, {textContent: totalsText});
var scriptTableT = scriptTable.parentNode; // This is the "table" element
scriptTableT.parentNode.insertBefore(totalsP, scriptTableT);
// Gets the last check date (LCD) and saves the current one
var checkDate = new Date(), checkDateOld = parseInt(GM_getValue("lastCheck", null), 10);
checkDateOld = (isNaN(checkDateOld)) ? null : (new Date(checkDateOld));
GM_setValue("lastCheck", checkDate.getTime().toString());
// If there's a saved LCD, creates a paragraph with it (and its "humanized" difference with the current date) and insert it into the document before the script table
if (checkDateOld !== null) {
var datesP = createNode("p", {class: "subtitle"},
{textContent: "Last check was " + getDateDiffString(checkDate, checkDateOld) + " ago (" +
checkDateOld.toUTCString() + ")"});
scriptTableT.parentNode.insertBefore(datesP, scriptTableT);
}
