您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds checkboxes to Google Play to filter app reviews based on their star rating out of five
当前为
- // ==UserScript==
- // @name Google Play Review Rating Filter
- // @version 1.0.0
- // @description Adds checkboxes to Google Play to filter app reviews based on their star rating out of five
- // @icon https://s2.googleusercontent.com/s2/favicons?domain=https%3A%2F%2Fplay.google.com
- // @namespace http://www.qzdesign.co.uk/userscripts/
- // @include https://play.google.com/*
- // @include http://play.google.com/*
- // @run-at document-end
- // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js
- // @grant none
- // ==/UserScript==
- //'use strict';
- /// false = no debug logging, true = some debug logging, 2 = extra debug logging
- var debug = false;
- try {
- if (debug) console.log('GPRRF included');
- // Avoid any possible jQuery conflict
- this.$ = this.jQuery = jQuery.noConflict(true);
- /**
- * Set up prototypal inheritance.
- * @param sup `Function` that is the superclass's constructor
- * @param methods Object whose properties are methods to set in the prototype
- */
- Function.prototype.gprrfExtend = function(sup, methods) {
- this.prototype = Object.create(sup.prototype);
- this.prototype.constructor = this;
- $.extend(this.prototype, methods);
- };
- /**
- * Class for hooking `XMLHttpRequest`
- */
- var XMLHttpRequestHook = (function() {
- /**
- * Constructor
- * @param initData Object whose properties will be initially copied to the
- * instance
- */
- function XMLHttpRequestHook(initData) {
- if (initData) {
- $.extend(this, initData);
- }
- activeHooks.push(this);
- }
- /** Array of currently active `XMLHttpRequestHook` objects */
- var activeHooks = [];
- /** Stores the original `XMLHttpRequest.prototype.open` */
- var xhrOpen = XMLHttpRequest.prototype.open;
- /** Like `Object.getOwnPropertyDescriptor` but traverses prototype chain */
- var getPropertyDescriptor = function(obj, prop) {
- return Object.getOwnPropertyDescriptor(obj, prop) ||
- getPropertyDescriptor(Object.getPrototypeOf(obj), prop);
- };
- /** Function to override `XMLHttpRequest.prototype.open` with */
- var xhrOpenHook = function() {
- if (debug > 1) console.log('XHR[%o] open(%o)', this, arguments);
- if (debug > 1) console.log('activeHooks: %o', activeHooks);
- // See if any active `XMLHttpRequestHook` will handle the request
- for (var i = 0, l = activeHooks.length; i < l; ++i) {
- var xhrHook = activeHooks[i];
- // Prepend the `XMLHttpRequest` object to the arguments
- if (xhrHook.open.bind(xhrHook, this).apply(void 0, arguments)) {
- return;
- }
- }
- // Otherwise call the original `XMLHttpRequest` method
- return xhrOpen.apply(this, arguments);
- };
- // Assign the replacement `XMLHttpRequest.prototype.open` -
- // Handle running in a Sandbox (i.e. if included via Unified Script Injector)
- if (XMLHttpRequest.wrappedJSObject && typeof exportFunction !== 'undefined') {
- XMLHttpRequest.wrappedJSObject.prototype.open = exportFunction(
- xhrOpenHook,
- XMLHttpRequest.wrappedJSObject.prototype
- );
- } else {
- XMLHttpRequest.prototype.open = xhrOpenHook;
- }
- // Define `XMLHttpRequestHook` prototype methods
- XMLHttpRequestHook.gprrfExtend(Object, {
- /**
- * This method is called in response to a call to
- * `XMLHttpRequest.prototype.open()`. This default implementation hooks the
- * request so that the `send()` and `done()` methods will be called
- * accordingly, makes the `responseText`, `status` and `readyState`
- * properties writable, calls the original
- * `XMLHttpRequest.prototype.open()`, and returns `true`.
- * Overridden implementations should return `false` and not call this
- * implementation if the request should not be hooked.
- *
- * @param xhr The `XMLHttpRequest` object.
- * The other parameters are as for `XMLHttpRequest.prototype.open`
- *
- * @return `true` if the request has been hooked and the original
- * `XMLHttpRequest.prototype.open` should not now be called, `false`
- * otherwise.
- */
- open: function(xhr, method, url, async, user, password) {
- try {
- if (debug > 1) console.log('XHRH open(%o)', arguments);
- var xhrHook = this;
- /** Holder for `XMLHttpRequest` properties that will be overridden */
- var xhrProps = {
- // Store any value (or lack of) already set for `onreadystatechange`
- onreadystatechange: xhr.onreadystatechange
- };
- // Store original `send` property (usually from the prototype)
- xhr.orgSend = xhr.send;
- // Replace `XMLHttpRequest.prototype.send` for this instance
- xhr.send = function() {
- if (debug > 1) console.log('XHR[%o] send(%o)', this, arguments);
- // Prepend the `XMLHttpRequest` object to the arguments
- return xhrHook.send.bind(xhrHook, this).apply(void 0, arguments);
- };
- // Set the `onreadystatechange` function before redefining the property
- xhr.onreadystatechange = function() {
- if (debug > 1) console.log('XHR[%o] orsc(%o)', this, arguments);
- if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
- // Prepend the `XMLHttpRequest` object to the arguments
- return xhrHook.done.bind(xhrHook, this).apply(void 0, arguments);
- }
- // `xhr.onreadystatechange` will be the redefined property here
- if (xhr.onreadystatechange) {
- return xhr.onreadystatechange.apply(this, arguments);
- }
- };
- // Redefine `onreadystatechange` to have the value in `xhrProps`.
- // Make `responseText`, `status` and `readyState` properties writable.
- var props = [
- 'onreadystatechange', 'responseText', 'status', 'readyState'
- ];
- props.forEach(function(prop) {
- var orgDescriptor = getPropertyDescriptor(xhr, prop);
- Object.defineProperty(xhr, prop, {
- get: function() {
- if (prop in xhrProps) {
- return xhrProps[prop];
- } else if (orgDescriptor.get) {
- return orgDescriptor.get.call(this);
- } else {
- return orgDescriptor.value;
- }
- },
- set: function(v) {
- if (debug > 1) console.log('set prop %s = %o', prop, v);
- xhrProps[prop] = v;
- },
- configurable: true,
- enumerable: true
- });
- });
- // Handle running in a Sandbox (i.e. in Unified Script Injector)
- if (xhr.wrappedJSObject && typeof exportFunction !== 'undefined') {
- xhr.wrappedJSObject.send = exportFunction(
- xhr.send,
- xhr.wrappedJSObject
- );
- props.forEach(function(prop) {
- var pd = getPropertyDescriptor(xhr, prop);
- pd.set = exportFunction(pd.set, xhr.wrappedJSObject);
- pd.get = exportFunction(pd.get, xhr.wrappedJSObject);
- Object.defineProperty(xhr.wrappedJSObject, prop, pd);
- });
- }
- // Call original `XMLHttpRequest.prototype.open` with first argument as
- // `this` instead of being an argument
- Function.prototype.call.apply(xhrOpen, arguments);
- // The request has been hooked
- return true;
- }
- catch (e) {
- if (console && console.error) console.error('%o\n%o', e, e.stack);
- return false;
- }
- },
- /**
- * Called in response to a call to `send` on an `XMLHttpRequest` object
- * which was hooked by `XMLHttpRequestHook.prototype.open()`.
- *
- * @param xhr The `XMLHttpRequest` object.
- * The other parameters are as for `XMLHttpRequest.prototype.send`
- */
- send: function(xhr, data) {
- if (debug > 1) console.log('XHRH send(%o)', arguments);
- // Call original `xhr.send` with first argument as `this` instead of being
- // an argument
- return Function.prototype.call.apply(xhr.orgSend, arguments);
- },
- /**
- * Called in response to a chrome call to `onreadystatechange` for an
- * `XMLHttpRequest` object which was hooked by
- * `XMLHttpRequestHook.prototype.open()` when the `readyState` is
- * `XMLHttpRequest.DONE` and `status` is 200.
- *
- * If a content code has set an `onreadystatechange` function, this method
- * calls it after setting the `readyState` to `DONE` and `status` to 200, so
- * it can be called explicitly to return a response after setting the
- * `responseText` property.
- *
- * @param xhr The `XMLHttpRequest` object.
- */
- done: function(xhr) {
- if (debug > 1) console.log('XHRH done(%o)', arguments);
- xhr.status = 200;
- xhr.readyState = XMLHttpRequest.DONE;
- if (xhr.onreadystatechange) {
- // Call original `xhr.onreadystatechange` with first argument as `this`
- // instead of being an argument
- return Function.prototype.call.apply(xhr.onreadystatechange, arguments);
- }
- },
- /** Disable this hook */
- destruct: function() {
- activeHooks = activeHooks.filter(function(e) { return e !== this; });
- }
- });
- return XMLHttpRequestHook;
- })();
- $(function() {
- /**
- * Constructs a query string from an object's properties. Like
- * `jQuery.param()` but does not use `encodeURIComponent`, instead assuming
- * properties and values are already suitably URI-encoded. Google does not
- * expect spaces encoded as plus signs which is what it gets when
- * `jQuery.post()` is called with `data` as an object.
- */
- function param(obj) {
- var params = [];
- for (var k in obj) {
- if (obj.hasOwnProperty(k)) {
- params.push(k + '=' + obj[k]);
- }
- }
- return params.join('&');
- }
- /**
- * Perform the reverse of `param()`, i.e. construct object from query string
- */
- function deparam(queryString) {
- var obj = {};
- queryString.split('&').forEach(function(param) {
- var parts = param.split('=');
- var k = parts.shift();
- obj[k] = parts.join('='); // join in case of stray '='
- });
- return obj;
- }
- /**
- * Identity function. Useful for `Array.prototype.some` and
- * `Array.prototype.every` when used on an array of booleans.
- */
- function identityFunction(v) { return v; }
- /**
- * Add the checkboxes to the UI for selecting which review ratings to filter.
- * Grab the total review count from the page.
- */
- function initialize() {
- totalNumReviews = +$('span.reviews-num').text().replace(/[,.]/g, '');
- if (debug) console.log('Total number of reviews: %d', totalNumReviews);
- if ($ratingsFilters) {
- $ratingsFilters.remove();
- }
- $ratingsFilters = $('<div class="review-filter"/>')
- .appendTo('div.review-filters')
- .change(function() {
- // Trigger a click on the currently selected sort order item to get
- // Google's script to fetch reviews afresh
- $('.id-review-sort-filter .dropdown-child.selected').click();
- });
- for (var i = 1; i <= 5; ++i) {
- $('<label/>')
- .text(i + '*')
- .prepend($('<input/>').attr({
- type: 'checkbox',
- id: 'review-filter-' + i + '-star',
- checked: ''
- }))
- .appendTo($ratingsFilters);
- }
- }
- /**
- * Extend `XMLHttpRequestHook` to only hook POST requests in which the path
- * part of the URL matches a specific string
- *
- * @param urlPath string which the path part of the URL must match
- * @param initData instance data and methods to set as properties
- */
- function GooglePlayXMLHttpRequestHook(urlPath, initData) {
- XMLHttpRequestHook.call(this, initData);
- this.urlPath = urlPath;
- }
- // Define `GooglePlayXMLHttpRequestHook` prototype methods
- GooglePlayXMLHttpRequestHook.gprrfExtend(XMLHttpRequestHook, {
- /**
- * @return `true` if the request should be hooked.
- * Parameters as for `open`.
- */
- hook: function(xhr, method, url) {
- return method === 'POST' && url &&
- this.urlPath ===
- url.replace(/^(?:https?:)?\/\/play\.google\.com/, '').split('?', 1)[0]
- ;
- },
- open: function(xhr, method, url) {
- return this.hook.apply(this, arguments) &&
- XMLHttpRequestHook.prototype.open.apply(this, arguments)
- ;
- }
- });
- /** The total number of reviews */
- var totalNumReviews;
- /** jQuery object for element containing rating filter checkboxes */
- var $ratingsFilters;
- /** The main `XMLHttpRequestHook` for marshalling review data */
- var getReviewsHook = new GooglePlayXMLHttpRequestHook('/store/getreviews', {
- /**
- * Array of Objects containing data about each pending request. Each object
- * has the following properties:
- * - `pageNum`: The requested page number of the paged data;
- * - `postParams`: The parameters that are sent in the POST request body to
- * Google Play, but without the `pageNum` parameter;
- * - `filters`: Array of booleans indexed by 'star rating' indicating
- * whether reviews with this rating should be included in the results;
- * - `xhr`: The `XMLHttpRequest` object for the request;
- * - `fullDataKey`: Property key for `data` where data from unfiltered
- * results obtained from Google is stored per-page;
- * - `filteredDataKey`: Property key for `filteredData` where data about the
- * results obtained from Google, with the selected filters applied, is
- * stored per-page.
- */
- pendingRequests: [],
- /**
- * Object whose property keys are a serialized representation of request
- * parameters without the page number or any rating filter selection, and
- * whose values are an array of objects, indexed by page number, containing
- * review data retrieved via AJAX.
- */
- data: {},
- /**
- * Object whose property keys are a serialized representation of request
- * parameters, including rating filter selections, but without the page
- * number, and whose values are an array of objects, indexed by page number,
- * containing review data retrieved via AJAX filtered according to rating.
- */
- filteredData: {},
- /**
- * Grab the current set of rating filters selected in the UI.
- * Don't bother hooking the request if all ratings selected.
- * Store the URL to use for internal AJAX requests.
- */
- hook: function(xhr, method, url) {
- if (GooglePlayXMLHttpRequestHook.prototype.hook.apply(this, arguments)) {
- /** URL to use for internal AJAX requests */
- this.requestURL = url;
- // Which filters are selected?
- var filters = this.filters = [];
- $ratingsFilters.find('input').each(function() {
- filters[+(/^review-filter-(\d)-star$/.exec(this.id)[1])] =
- this.checked;
- });
- return !filters.every(identityFunction);
- } else {
- return false;
- }
- },
- /**
- * If the request is for review data, and is not an internal request made by
- * this script, create a pending request and then process pending requests
- * instead of calling `XMLHttpRequest.prototype.send()`.
- */
- send: function(xhr, data) {
- try {
- if (data) {
- var postParams = deparam(data);
- if (postParams.gprrfInternal) {
- // Internal request by this script
- delete postParams.gprrfInternal;
- return GooglePlayXMLHttpRequestHook.prototype.send.call(
- this,
- xhr,
- param(postParams)
- );
- } else {
- // Create a new request
- var rq = {
- pageNum: +postParams.pageNum,
- postParams: postParams,
- filters: this.filters,
- xhr: xhr
- };
- // Choose data keys unique to the set of parameters
- delete postParams.pageNum;
- var dataKeyParts = [];
- Object.keys(postParams).sort().forEach(function (key) {
- dataKeyParts.push(key + '=' + postParams[key]);
- });
- rq.fullDataKey = this.lastDataKey = dataKeyParts.join('&');
- rq.filteredDataKey =
- rq.fullDataKey + '&' + JSON.stringify(rq.filters);
- this.data[rq.fullDataKey] = this.data[rq.fullDataKey] || [];
- this.filteredData[rq.filteredDataKey] =
- this.filteredData[rq.filteredDataKey] || [];
- // Add request as pending and process, but do not call super:
- // `done()` will be called directly when the data is ready.
- this.pendingRequests.push(rq);
- if (debug) console.log('New request: %o', rq);
- return this.processPendingRequests();
- }
- }
- // Call super
- return GooglePlayXMLHttpRequestHook.prototype.send.apply(
- this,
- arguments
- );
- } catch (e) {
- if (console && console.error) console.error('%o\n%o', e, e.stack);
- }
- },
- /**
- * Process all pending requests. When there are no pending requests,
- * possibly reclaim memory by removing obsolete data.
- */
- processPendingRequests: function() {
- this.pendingRequests = this.pendingRequests.filter(
- this.processRequest,
- this
- );
- if (!this.pendingRequests.length) {
- // Reclaim memory by discarding data obtained with different parameters
- [this.data, this.filteredData].forEach(function(d) {
- for (var k in d) {
- if (k.substr(0, this.lastDataKey.length) !== this.lastDataKey) {
- if (debug) console.log('Removing data with key %s', k);
- delete d[k];
- }
- }
- }, this);
- }
- },
- /**
- * Process a request. If there is enough data to satisfy the request,
- * `done()` will be called with responseText set to the result, otherwise
- * more data will be requested via AJAX.
- *
- * @param rq Object containing data about the request
- *
- * @return `true` if the request is still pending, `false` if it is complete
- * or has failed (so the method can be used with `Array.prototype.filter`).
- */
- processRequest: function(rq) {
- function debugInfo() {
- return 'spn=' + srcPageNum + ' dpn=' + destPageNum +
- ' dprc=' + destPageReviewCount + ' dppc=' + destPagePageCount;
- }
- try {
- // Iterate through source pages working out how many are needed to get
- // enough reviews on each destination page. Request source page data as
- // required. When there is enough data to return for the requested
- // destination page, return it. This involves iterating at least to the
- // next page to determine if the requested page would be the last.
- // This is slightly inefficient because after data is requested, the
- // iteration will begin at the start again when it is received.
- var srcPageNum = 0;
- var destPageNum = 0;
- var destPageReviewCount = 0;
- var destPagePageCount = 0;
- var nodesToInclude = [];
- for (;;) {
- var prevSrcPageData = srcPageData;
- var srcPageData = this.getFilteredSrcPageData(rq, srcPageNum);
- if (!srcPageData) {
- if (debug) console.log('requested more data: %s', debugInfo());
- return true; // Data has been requested, come back when it arrives
- }
- // Update counts for current destination page
- destPageReviewCount += srcPageData.numReviews;
- ++destPagePageCount;
- if (debug > 1) console.log('in loop: %s', debugInfo());
- // Include filtered nodes if destination page is requested page
- if (destPageNum === rq.pageNum) {
- Array.prototype.push.apply(nodesToInclude, srcPageData.nodes);
- }
- // Advance to next destination page if enough reviews found for
- // current:
- // - Try to get at least 3 reviews;
- // - On the first page, aim for more than will fit on the screen to
- // avoid a Google bug where the next page is immediately requested
- // but the requested page number is one after the last page that
- // was requested for the previous result set;
- // - If there is at least one review, don't request more than 5 pages
- // worth of data looking for more (too many rapid requests may
- // trigger a temporary IP ban by Google);
- // - Bail after 10 pages of data have not yielded any reviews, for the
- // same reason;
- if (
- destPageReviewCount >= (destPageNum === 0 ? 7 : 3) ||
- destPageReviewCount && destPagePageCount >= 5 ||
- destPagePageCount >= 10
- ) {
- if (debug) console.log('next dest page: %s', debugInfo());
- ++destPageNum;
- destPageReviewCount = 0;
- destPagePageCount = 0;
- }
- // Done?
- var anyFilter = anyFilter || rq.filters.some(identityFunction);
- if (
- // End if there are no more source pages
- !srcPageData.fullData.numReviews ||
- srcPageData.fullData.responseCode == 2 ||
- // End if on or past a non-empty destination page after the one
- // requested
- destPageNum > rq.pageNum + 1 ||
- destPageNum > rq.pageNum && destPageReviewCount ||
- // End if all filters unchecked.
- // Wait until now so that there is a JSON data template to use.
- !anyFilter
- ) {
- break;
- }
- // Advance to next source page
- if (rq.postParams.reviewSortOrder == 1 && !srcPageData.numReviews) {
- // No reviews on this page, and sorting by rating.
- // Binary search to find next page with reviews.
- // What rating to look for next?
- var ratingSought = 0;
- for (var i = 1; !srcPageData.fullData.hasRating[i] && i <= 5; ++i) {
- if (rq.filters[i]) {
- ratingSought = i;
- }
- }
- var pageBefore = 0;
- var pageAfter = Math.floor(
- (totalNumReviews - 1) /
- this.data[rq.fullDataKey][0].numReviews + 1
- );
- var pageOutOfRange = false;
- while (pageBefore + 1 < pageAfter) {
- // Try to get there more quickly if last page out of range
- // - there could be millions of reviews but only a few in the set
- var midPage = pageOutOfRange && pageBefore + 3 < pageAfter
- ? Math.floor((pageAfter + pageBefore * 3) / 4)
- : Math.floor((pageAfter + pageBefore) / 2);
- var midPageData = this.getFilteredSrcPageData(rq, midPage);
- if (!midPageData) {
- return true; // Data has been requested, come back later
- }
- pageOutOfRange = !midPageData.fullData.numReviews;
- if (pageOutOfRange) {
- pageAfter = midPage;
- } else {
- // If there are ratings equal or below the desired rating,
- // the mid page is after the sought page
- for (var i = 1; i <= ratingSought; ++i) {
- if (midPageData.fullData.hasRating[i]) {
- pageAfter = midPage;
- break;
- }
- }
- if (pageAfter !== midPage) {
- pageBefore = midPage;
- }
- }
- }
- if (debug) console.log('spn: %d -> %d', srcPageNum, pageAfter);
- if (debug) console.assert(pageAfter > srcPageNum, 'Error!');
- srcPageNum = pageAfter;
- } else {
- ++srcPageNum;
- }
- }
- // Send the response to the content code...
- // Need to indicate if this is the last page.
- var isLastPage = destPageNum <= rq.pageNum ||
- destPageNum <= rq.pageNum + 1 && !destPageReviewCount;
- // Use some data obtained from Google as a template, so the response is
- // as consistent as possible with what is expected.
- var templateSrcPageData = (
- isLastPage && srcPageData.responseText || !prevSrcPageData
- ? srcPageData : prevSrcPageData
- ).fullData;
- var jsonData = [[
- templateSrcPageData.responseText, ///< "ecr"
- // Ensure 2 is returned for the last page
- isLastPage && templateSrcPageData.responseCode != 2
- ? 2 : templateSrcPageData.responseCode,
- // Ensure something is returned if there are no reviews, otherwise
- // the UI doesn't get updated
- $('<body/>').append(
- nodesToInclude.length || rq.pageNum
- ? $(nodesToInclude).clone()
- : $('<p/>').text(
- rq.postParams.reviewSortOrder == 1
- ? 'No matching reviews could be obtained'
- : 'No reviews found'
- )
- ).html(),
- rq.pageNum
- ]];
- if (debug) console.log('done: %s ilp=%o', debugInfo(), isLastPage);
- if (debug) console.log(
- 'template=%o, nodes=%o, response=%o, this=%o',
- templateSrcPageData, nodesToInclude, jsonData, this
- );
- // Inlcude the preceding junk ")]}'\n\n" in the response
- rq.xhr.responseText =
- templateSrcPageData.junkBefore + JSON.stringify(jsonData);
- // Use `setTimeout` in case of immediate result in `send()`
- setTimeout(this.done.bind(this, rq.xhr), 0);
- // Request has been processed, remove it from 'pending' list
- return false;
- }
- catch (e) {
- if (console && console.error) console.error('%o\n%o', e, e.stack);
- return false;
- }
- },
- /**
- * Obtain and cache data for a page of reviews with a particular set of
- * parameters, filtered according to the requested ratings. If data is not
- * yet available, it is requested via AJAX.
- *
- * @param rq An object containing parameters and details of the request
- * @param pageNum The page index of the source data to return
- *
- * @return If the data is not yet available, the return value is undefined.
- * Otherwise, an object with the following properties is returned:
- * - `nodes`: An array of `HTMLElement` objects which are either reviews
- * which pass the filters or developer replies thereof;
- * - `numReviews`: The number of reviews in the filtered page data;
- * - `fullData`: The unfiltered data, an object with these properties:
- * - `$domFragment`: A jQuery object which contains one HTML element whose
- * children are the reviews and developer replies;
- * - `numReviews`: The number of reviews on this page;
- * - `hasRating`: An array indexed by rating whose values are `true` if
- * there are any reviews on the page with this rating; this information
- * is used when binary-searching for the next page containing reviews
- * that will pass filtering when reviews are sorted by rating;
- * - `responseCode`: 1 = ok, 2 = last page, 3 = error, undefined if HTTP
- * error or response not as expected;
- * - `responseText`: Always seems to be "ecr";
- * - `junkBefore`: All responses seem to be preceded with junk ")]}'\n\n".
- */
- getFilteredSrcPageData: function(rq, pageNum) {
- var data = this.data[rq.fullDataKey][pageNum];
- if (data) {
- var filteredData = this.filteredData[rq.filteredDataKey][pageNum];
- if (!filteredData) {
- filteredData = {
- fullData: data,
- nodes: [],
- numReviews: 0
- };
- data.hasRating = data.hasRating || [];
- data.$domFragment.find('div.current-rating').each(function() {
- var $this = $(this);
- var rating = Math.round(parseInt($this.css('width')) / 20);
- data.hasRating[rating] = true;
- if (rq.filters[rating]) {
- // Include this review, and any developer reply
- if (debug > 1) console.log('%d* review included', rating);
- var $review = $this.closest('div.single-review');
- Array.prototype.push.apply(
- filteredData.nodes,
- $review.add($review.next('div.developer-reply')).get()
- );
- ++filteredData.numReviews;
- }
- });
- if (debug) console.log(
- 'filtered data for page %d: %o',
- pageNum, filteredData
- );
- this.filteredData[rq.filteredDataKey][pageNum] = filteredData;
- }
- return filteredData;
- } else {
- if (debug > 1) console.log('Doing POST for source page %d', pageNum);
- $.post({
- url: this.requestURL,
- data: param($.extend({}, rq.postParams, {
- pageNum: pageNum,
- gprrfInternal: true ///< Flag not to hook this request
- })),
- dataType: 'text'
- }).always(this.receiveData.bind(this, rq.fullDataKey, pageNum));
- }
- },
- /**
- * Store data received via AJAX, then reprocess pending requests
- *
- * @param dataKey Serialized representation of the request parameters
- * to use as the property key of data to set
- * @param pageNum The source page number of the data
- * @param data String containing the data received on success; on failure
- * this will be a `jqXHR` object
- */
- receiveData: function(dataKey, pageNum, data) {
- var dataObj = {};
- if (typeof data === 'string') {
- var skipJunk = data.indexOf('[');
- try {
- var jsonData = JSON.parse(skipJunk ? data.substr(skipJunk) : data);
- if (
- jsonData.length > 0 &&
- jsonData[0].length > 3 &&
- jsonData[0][3] == pageNum
- ) {
- dataObj.$domFragment = $('<body/>').html(jsonData[0][2]);
- dataObj.responseText = jsonData[0][0]; // "ecr"
- dataObj.responseCode = jsonData[0][1]; // 1 ok, 2 last page, 3 error
- }
- } catch (e) {
- if (debug) console.error(e);
- }
- dataObj.junkBefore = data.substr(0, skipJunk); // ")]}'\n\n"
- }
- dataObj.$domFragment = dataObj.$domFragment || $([]);
- dataObj.numReviews =
- dataObj.$domFragment.find('div.single-review').length;
- if (debug) console.log(
- 'received page %d, key %s: %o',
- pageNum, dataKey, dataObj
- );
- this.data[dataKey][pageNum] = dataObj;
- this.processPendingRequests();
- }
- });
- /**
- * Reinitialize when a new App page is loaded via AJAX
- */
- var loadPageHook = new GooglePlayXMLHttpRequestHook('/store/apps/details', {
- /**
- * Hook if the request contains a URL parameter `psid` with value 2, which
- * indicates a request for new App review content.
- */
- hook: function(xhr, method, url) {
- if (GooglePlayXMLHttpRequestHook.prototype.hook.apply(this, arguments)) {
- var urlParts = url.split('?', 2);
- if (debug) console.log(urlParts);
- return urlParts.length > 1 && deparam(urlParts[1]).psid == 2;
- } else {
- return false;
- }
- },
- done: function(xhr) {
- if (debug) console.log(xhr.responseURL);
- if (debug > 1) console.log(xhr.responseText);
- GooglePlayXMLHttpRequestHook.prototype.done.apply(this, arguments);
- initialize();
- }
- });
- // Initialize UI and review count for the page when first loaded
- initialize();
- });
- } catch (e) {
- if (console && console.error) console.error(
- 'Error at line %d:\n%o\n%o', e.lineNumber, e, e.stack
- );
- }