NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name YouTube Comments Plus
// @namespace jojje/gm
// @description Fetches all comments and allows for inverting the sorting order
// @require http://code.jquery.com/jquery-1.9.1.min.js
// @include https://www.youtube.com/watch?v=*
// @downloadURL https://raw.githubusercontent.com/jojje/ytc-plus/master/youtube_comments_plus.user.js
// @updateURL https://raw.githubusercontent.com/jojje/ytc-plus/master/youtube_comments_plus.user.js
// @homepageURL https://github.com/jojje/ytc-plus
// @author jojje
// @version 1.0
// @grant none
// ==/UserScript==
var anchorFilter = '#yt-comments-order-button';
var state = "idle";
//==============================================
// Main logic
//==============================================
function updateProgress(n, total) {
var pct = Math.round(100*n/total),
spct = ''+ pct +'%';
$('#f-percent').html(spct);
$('#f-bar').css('width',spct);
$('#f-progress').show();
log("updating stats: "+ n + "/"+ total +": " + spct);
}
function hideProgress() {
$('#f-progress').fadeOut('slow');
}
function fetchReplies() {
$('.load-comments:visible').each(function() {
var self = $(this), observer;
observer = onAttributeChange(self[0], function() { // Element changes class attribute when replies are fetched
if(self.hasClass('hid')) { // by adding hid class.
observer.disconnect(); // Stop listening to this reply-set since it's now done.
log('loaded comments for ', self[0]);
notify('stats.stale');
if ( $('.load-comments:visible').length == 0) { // last step is fetching the replies, so if there are no more to get, we're done with all the XHR fetching
notify('replies.fetched');
}
}
});
}).click(); // Trigger fetching of all unfetched (hidden/collapsed) replies.
}
function getPager() {
return $('#yt-comments-paginator');
}
function fetchMore() {
state = 'fetching';
getPager().click();
}
function invertOrder() { // Invert the order of the comments. Replies are already in a sane order (chronoligical)
var comments = $('.comment-entry');
comments.parent().first().append( comments.get().reverse() );
}
function getTotalCommentCount() {
var m = $('.all-comments > a').text().trim().match(/\(([,.\d]+)\)/);
if(m) return parseInt(m[1].replace(/[.,]/g,''),10);
log('Error: Failed to find total number of comments');
}
function getFetchedCommentCount() {
return $('.comment-item').length;
}
function addListeners() {
var pager = getPager();
$('#b-fetchall').click(function() {
$(this).hide();
notify('stats.stale');
fetchMore();
});
$('#b-sort').click(invertOrder);
if(!pager.is(':visible')) notify('all.fetched'); // If there are to few comments to warrant a pager, there's nothing to fetch, so we're done.
onAttributeChange(pager[0], function() { // else, there are more comments to fetch.
if(state != 'fetching') return; // Only act if the paginated fetching was started using the script's button, not by user clicking "Show more".
if(!pager.is(':visible')) notify('all.fetched'); // We've fetched all comments.
else if(!pager.hasClass('activated')) notify('page.fetched'); // Google removes this class when it has injected additional comments.
else notify('fetching.page'); // Pager is visible and has the activated class, so it's currently fetching a comment page.
});
on('page.fetched', function(){
notify('stats.stale');
fetchMore();
});
on('all.fetched', function() {
log('all fetched');
notify('stats.stale');
fetchReplies();
});
on('fetching.page', function() {
log('fetching page');
});
on('stats.stale', function() {
updateProgress(getFetchedCommentCount(), getTotalCommentCount());
});
on('replies.fetched', hideProgress);
}
//==============================================
// Helpers
//==============================================
function log() {
return;
if(typeof(console) != 'undefined' && console.debug) {
var stamp = (new Date()).toLocaleString().split(' ').pop(),
args = Array.prototype.slice.call(arguments);
console.debug.apply(this,[stamp].concat(args));
}
}
function addUI() {
$('<style type="text/css">'+
' #c-fetchall { display:inline; margin-left:0.2em; }'+
' #c-fetchall button { margin-left:0.2em; }'+
' #f-progress { border:1px solid black; display:inline-block; height:1.5em; margin-left:0.2em; margin-top:0.25em; position:absolute; width:4.8em; }'+
' #f-percent { left:34%; padding-top:0.25em; position:absolute; }'+
' #f-bar { height:1.5em; background-color:#99ff99; width:50%; }'+
'</style>').appendTo('body');
$('<div id="c-fetchall">').insertAfter('#yt-comments-order-button');
$('<button id="b-sort" class="yt-uix-button yt-uix-button-default" title="Reverse the comment sorting order"><span>Reverse</span></button>')
.appendTo('#c-fetchall');
$('<button id="b-fetchall" class="yt-uix-button yt-uix-button-default" title="Fetch all comments and replies"><span>Fetch all</span></button>')
.appendTo('#c-fetchall');
$('<div id="f-progress" style="display:none"><span id="f-percent">50%</span><div id="f-bar"></div></div>')
.appendTo('#c-fetchall');
}
function onAttributeChange(el,cb) {
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver,
observer = new MutationObserver(cb);
observer.observe(el, {
attributes: true
});
return observer;
}
function notify(eventName) { $(document.body).trigger(eventName); }
function on(eventName, cb) { $(document.body).on(eventName, cb); }
//==============================================
// Bootstrap
//==============================================
function onReady(cb) {
var i = setInterval(function() {
if( $(anchorFilter)[0] ) {
clearInterval(i);
cb();
}
},100);
}
onReady(function() {
log('yt enhancement initializing');
addUI();
log('ui added');
addListeners();
log('yt enhancement initialized');
});