Skip to content

Instantly share code, notes, and snippets.

@hlfbt
Last active May 1, 2017 00:37
Show Gist options
  • Save hlfbt/07e2f04dacf6657c0087 to your computer and use it in GitHub Desktop.
Save hlfbt/07e2f04dacf6657c0087 to your computer and use it in GitHub Desktop.
Shows toasty notifications for various things on AnimeBytes! See the comments for screenshots!
// ==UserScript==
// @name AnimeBytes Notifications
// @author potatoe
// @version 2.6.3.X.4.11
// @description AnimeBytes SUPER Notifications! super! s-...super..? || Shows toasty notifications for various things!
// @icon https://animebytes.tv/favicon.ico
// @include https://animebytes.tv/*
// @match https://animebytes.tv/*
// @downloadURL https://ab.nope.bz/userscripts/notifications/ab_notifications.beta.user.js
// @updateURL https://ab.nope.bz/userscripts/notifications/ab_notifications.beta.user.js
// @grant none
// ==/UserScript==
/*
* Check wether the Browser supports Notifications and if they are granted for AnimeBytes
*/
if (!("Notification" in window)) alert("Notifications are not supported by your browser!");
if (Notification.permission !== "granted")
document.getElementById('alerts').innerHTML +=
"<div id='notificationsalert' class='alertbar message' style='background: #FFB2B2; border: 1px solid " +
"#D45A5A;'><a href='javascript:Notification.requestPermission(function () {document.getElementById(\"" +
"notificationsalert\").parentNode.removeChild(document.getElementById(\"notificationsalert\"));});' s" +
"tyle='color: #E40000;'>Enable Notifications</a></div>;";
if (Notification.permission === "denied")
alert("Notifications are denied for animebytes.tv, please reenable them in your browsers settings or disable/remove this userscript (AnimeBytes Notifications).");
/*
* Define some basic needed Functions and Objects
*/
var toastNotificationIconRead = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2F'
+ 'yZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHdJREFUeNpi/P//PwM6eP36tSCQUoJy34uKit5DV8PCgB2sAmIX'
+ 'JD4jugImHBoFGQgAXBqRnfaeFI0M5NpItsb35Dr1PQ7/IoIZWzxC49IYphEYj++JshGoKQ1I7YbiDmxqcCUAJ'
+ 'aS4NCY3VN9T1Y84NRICAAEGANZrKAP0cRsiAAAAAElFTkSuQmCC';
var toastNotificationIconUnread = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2F'
+ 'yZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOZJREFUeNpi/P//PwM6+JSxUJZRXkgQzPn84xtvW+gddDUsDFgA'
+ 'IwdrL+PrL6HIQsRpZGUWZctyBKv+OW0/NiUMTNhFGa8x/PjN8B+IGVmYn2BTgtVGBmaged9/gZn/mbGbjV0jK'
+ '1Dx118MoGBjZGYkQSMz87f/QI1gT7IwfyBeIwvT279n7sPY14jWyPjh+6y/isKbGUGO/fjjPdGh+l9Lwpr588'
+ '8djJ9/7WBkZ80n3sbPP7VYXDRlQOzfh26ZkhCqzAx/9t2EGMLJ+op4GxkYZv6T4NkMYjH+/vsOqxpsiZwYABB'
+ 'gAIYRRu3OmvOQAAAAAElFTkSuQmCC';
// jQuery is $j on AnimeBytes
$ = $j;
this.GM_getValue = function (key) { return localStorage[key]; };
this.GM_setValue = function (key, val) { return localStorage[key] = val; }
this.GM_deleteValue = function (key) { return delete localStorage[key]; }
this.GM_addStyle = function (css) { var style=document.createElement('style'); style.textContent=css; document.head.appendChild(style); };
// Export objects to a global scope
function ex(obj, name) {
name = (name || obj.name);
if (name) {
window[name] = obj;
try { unsafeWindow[name] = obj; } catch (e) {}
}
}
// Simple custom NetworkError that gets thrown in case a XMLHttpRequest fails
function NetworkError (message) {
this.name = "NetworkError";
this.message = (message || "");
}
NetworkError.prototype = Error.prototype;
// Custom UserscriptEvent that can be fired for various custom Events. Most notably error Events!
// UserscriptEvent not yet fully supported (dispatchEvent() doesn't seem to accept any other than the standard Events).
/*UserscriptEvent = function (type, source, obj) {
if (typeof type === "undefined") throw new TypeError("Failed to construct 'UserscriptEvent': An event name must be provided.");
if (typeof obj !== "undefined" && typeof obj !== "object") throw new TypeError("Failed to construct 'UserscriptEvent': Argument is not an object.");
var evt = new Event(type);
for (var k in evt) this[k] = evt[k];
for (var k in obj) this[k] = obj[k];
this.source = (source || "AnimeBytes Notifications");
};*/
var UserscriptEvent = function (type, source, obj) {
if (typeof type === "undefined") throw new TypeError("Failed to construct 'UserscriptEvent': An event name must be provided.");
if (typeof obj !== "undefined" && typeof obj !== "object") throw new TypeError("Failed to construct 'UserscriptEvent': Argument is not an object.");
var evt = new CustomEvent(type);
for (var k in obj) evt[k] = obj[k];
evt.name = "UserscriptEvent";
evt.source = (source || "AnimeBytes Notifications");
return evt;
};
UserscriptEvent.prototype = CustomEvent.prototype;
// Don't you hate it to build a new XMLHttpRequest every single time you want to fetch a site?!
// Yea, I do too...
// Depends on NetworkError
function XHRWrapper (location, callback, method, sync, data) {
if (this.constructor != XHRWrapper) throw new TypeError("Constructor XHRWrapper requires 'new'");
this.name = "XHRWrapper";
this.typeOf = function () { return this.name; };
this.toString = function () { return '[object ' + this.name + ']'; };
this.valueOf = function () { return this; };
var xhr = new XMLHttpRequest();
Object.defineProperty(this, 'xmlHttpRequest', {
get: function () { return xhr; },
set: function () { return xhr; }
});
xhr.onerror = function(e) {
var netErr = new Error("A Network Error occured while loading the resource");
// Custom error code handling if client error occurs
var xhr = e.target;
if (xhr.status == 331) netErr.message += "System went to sleep/was suspended";
netErr.xmlHttpRequestProgressEvent = e;
netErr.xmlHttpRequest = xhr;
if (typeof error !== "undefined") { error = netErr; }
else { throw netErr; }
};
xhr.onreadystatechange = function(xhrpe) {
var xhr = xhrpe.target;
if (xhr.readyState == 4 && xhr.status == 200) {
var doc = document.implementation.createHTMLDocument();
doc.documentElement.innerHTML = xhr.responseText;
xhr.__callback(doc);
}
else if (xhr.readyState == 4 && xhr.status != 200) {
var netErr = new NetworkError();
// Custom error code handling if server error occurs
console.log("Error Code " + xhr.status + " (" + xhr.statusText + ")");
if (xhr.status == 331) netErr.message = "System went to sleep/was suspended";
throw netErr;
}
};
this.location = xhr.__location = location;
this.callback = xhr.__callback = callback;
xhr.open(method || 'GET', location, (typeof sync==="undefined")?true:sync);
xhr.send(data);
return this;
}
// Make it completely seemless. For this we cheat a bit by simply copying all property names in a new XHR and assigning them getters/setters to our private XHR Object.
// This does have the drawback that the 'on...' handlers will be the same for both the Wrapper and XHR, but that's not a problem for this usecase.
for (var key in new XMLHttpRequest()) {
if (!(key in ['addEventListener', 'removeEventListener', 'dispatchEvent']) && key !== key.toUpperCase()) { // Those should be seperate thought! Same with constants.
Object.defineProperty(XHRWrapper.prototype, key, {
get: new Function("return this.xmlHttpRequest." + key + ";"),
set: new Function('val', "return this.xmlHttpRequest." + key + " = val;")
});
}
}
// Make some noise!
function CustomAudio (src) {
this.audio = document.createElement('audio');
if (arguments.length > 0) this.audio.src = src;
if (Object.defineProperty) { // The proper way
Object.defineProperty(this, 'src', {
get: function () { return this.audio.src; },
set: function (s) { if (arguments.length < 1) { this.audio.removeAttribute('src'); } else { this.audio.src = s; } return s; }
});
} else { // The deprecated way
this.__defineGetter__('src', function () { return this.audio.src; });
this.__defineSetter__('src', function (s) { if (arguments.length < 1) { this.audio.removeAttribute('src'); } else { this.audio.src = s; } return s; });
}
this.play = function() { this.audio.play(); };
return this;
}
// This helps initializing all the GM variables
// Sets 'def' to 'gm' or returns 'gm's value if already set
// 'json' controlls wether the variable is/should be encoded as JSON String
// 'overwrite' controlls wether to overwrite 'gm' if it is not the same type as 'def'
function initGM(gm, def, json, overwrite) {
if (typeof def === "undefined") throw "shit";
if (typeof overwrite !== "boolean") overwrite = true;
if (typeof json !== "boolean") json = true;
var that = GM_getValue(gm);
if (that != null) {
var err = null;
try { that = ((json)?JSON.parse(that):that); }
catch (e) { if (e.message.match(/Unexpected token .*/)) err = e; }
if (!err && Object.prototype.toString.call(that) === Object.prototype.toString.call(def)) { return that; }
else if (overwrite) {
GM_setValue(gm, ((json)?JSON.stringify(def):def));
return def;
} else { if (err) { throw err; } else { return that; } }
} else {
GM_setValue(gm, ((json)?JSON.stringify(def):def));
return def;
}
}
// Parse CSS rules out of a string
// Returned rules length is 0 if the string didn't contain any valid CSS
// currently unused
function stringToCss(style) {
var doc = document.implementation.createHTMLDocument(""),
styleEle = document.createElement("style");
styleEle.textContent = style;
doc.body.appendChild(styleEle);
return styleEle.sheet.cssRules;
}
// Convert CSSRuleList to a string again
// currently unused
function cssToString(style) {
var cssStr = "";
for (var i = 0; i < style.length; i++)
cssStr += " " + style[i].cssText;
return cssStr.substring(1);
}
// A simple function to check if an object has a specific value
function hasVal(obj, val, strict, ignorePrototype) {
if (arguments.length < 3) strict = false;
if (arguments.length < 4) ignorePrototype = true;
for (var p in obj) {
if (ignorePrototype && !obj.hasOwnProperty(p)) continue;
if (strict) {
if (obj[p] === val) return true;
} else {
if (obj[p] == val) return true;
}
}
return false;
}
// Hashing function. Source: http://stackoverflow.com/a/7616484/929584
function hashCode(str) {
var hash = 0, i, chr, len;
if (str.length == 0) return hash;
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return ("00000000" + hash.toString(16).replace('-', '')).substr(-8);
}
// Convert the toast list returned by ToastHistory.getLast() into html
function lastToHtml(last) {
if (last.length < 1)
return '';
var html = '';
var tick, tock, date;
for (var l in last) {
tock = last[l];
if (html.substr(-6) === '</div>')
html += '\n<br>';
if (tock.ticks.length > 0)
html += '\n<div>\n<dt class="toast-day">' + tock.day + '</dt>\n<dd>\n';
for (var i in tock.ticks)
html += (tick = tock.ticks[i],
date = new Date(tick.time * 1e3),
date = date.toDateString() + ', ' + date.toTimeString().split(' ')[0],
new Toast(tick.data).html('toast' + tick.hash, date) + '\n');
if (tock.ticks.length > 0)
html += '</dd>\n</div>';
}
return html;
}
// Add the toast menu button to the page
// The button is a little exclamation mark in the header
// Upon clicking it, a menu that displays recent toasts will open
// I designed the toasts to look as similar to the chrome notifications as possible
function addExclamation() {
var bgc = $('#userinfo .subnav').css('background-color');
var html = '<li id="toastmenu">\n'
+ ' <img style="vertical-align: middle;" id="toast-notification-icon" src="' + toastNotificationIconRead + '">\n'
+ (bgc
? ' <ul id="toastdroppad" class="subnav"></ul>'
: '')
+ ' <ul id="toastdrop" class="subnav nobullet" style="display: none; !important">\n'
+ ' <div>\n'
+ ' <div id="toastempty">none :(</div>\n'
+ ' <div id="toastlist"></div>\n'
+ ' </div>\n'
+ ' </ul>\n'
+ '</li>';
var css = ''
// Toastlist CSS
+ '@import url(https://fonts.googleapis.com/css?family=Open+Sans:300);\n'
+ 'ul#toastdrop {\n'
+ ' cursor: default;\n'
+ ' min-height: 53px;\n'
+ ' max-height: 500px;\n'
+ ' width: 400px;\n'
+ ' margin-left: 5px !important;\n'
+ ' box-shadow: 0 0 5px rgba(0, 0, 0, 0.6);\n'
+ ' overflow-x: hidden !important;\n'
+ (bgc
? ' top: 26px !important;\n'
+ ' z-index: 1;\n'
: '')
+ '}\n'
+ (bgc
? 'ul#toastdroppad.subnav {\n'
+ ' width: 0;\n'
+ ' min-width: 0;\n'
+ ' height: 0;\n'
+ ' margin-left: 2px;\n'
+ ' border-left: 6px solid transparent;\n'
+ ' border-right: 6px solid transparent;\n'
+ ' border-bottom: 6px solid ' + bgc + ';\n'
+ ' border-radius: initial;\n'
+ ' background: none;\n'
+ ' float: initial;\n'
+ ' padding: 0;\n'
+ ' position: relative;\n'
+ ' top: 0;\n'
+ ' left: 0;\n'
+ ' z-index: 2;\n'
+ '}\n'
: '')
+ 'div#toastempty {\n'
+ ' width: 100%;\n'
+ ' height: 0;\n'
+ ' margin: 15px 0 -15px 0;'
+ ' font-family: "Open sans", sans serif;\n'
+ ' font-weight: 100;\n'
+ ' font-size: 25px;\n'
+ ' text-align: center;\n'
+ ' color: rgba(155, 155, 155, 0.2);\n'
+ '}\n'
+ 'div#toastlist {\n'
+ ' max-height: 475px;\n'
+ ' margin-right: -100px;\n'
+ ' padding: 0 100px 5px 5px;\n'
+ ' width: 405px;\n'
+ ' overflow-y: scroll !important;\n'
+ ' overflow-x: hidden !important;\n'
+ '}\n'
// Toast CSS
+ '@import url(https://fonts.googleapis.com/css?family=Roboto:300&subset=latin,latin-ext);\n'
+ '\n'
+ '.toast {\n'
+ ' cursor: default;\n'
+ ' position: relative;\n'
+ ' background-color: #fff;\n'
+ ' margin-bottom: 5px;\n'
+ ' box-shadow: 0px 1px 1px 1px rgba(0,0,0,0.2);\n'
+ ' width: 360px;\n'
+ ' min-height: 80px;\n'
+ ' max-height: 120px;\n'
+ '}\n'
+ '.toast-icon {\n'
+ ' display: inline-block;\n'
+ ' margin: -20px;\n'
+ ' padding: 40px !important;\n'
+ '}\n'
+ '.toast-iconwrapper {\n'
+ ' position: absolute;\n'
+ ' left: 0;\n'
+ ' top: 0;\n'
+ ' background-color: #eee;\n'
+ ' width: 80px;\n'
+ ' height: 80px;\n'
+ ' overflow: hidden;\n'
+ '}\n'
+ '.toast-content {\n'
+ ' font-family: Roboto, sans-serif;\n'
+ ' font-weight: 300;\n'
+ ' font-size: 13px;\n'
+ ' color: black;\n'
+ ' word-break: break-all;\n'
+ ' overflow-wrap: break-word;\n'
+ ' overflow: hidden;\n'
+ ' padding: 10px 10px 10px 95px;\n'
+ ' width: 255px;\n'
+ ' min-height: 60px;\n'
+ ' max-height: 100px;\n'
+ '}\n'
+ '.toast-close {\n'
+ ' cursor: pointer;\n'
+ ' background-image: url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0I'
+ 'Ars4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACNSURBVChTXY4xDsMgFEO5GexhyQk4WTICYsnCFrJkyh'
+ 'Cuw4KQK381bdUnWVjG/4PqvWPfd/xz37ecqpQCYwyWZZGA0GutUWuFYhBCgLUW67qKpmmC917KUiApJczzLKJ/+BRijHDOiegfpMC1n'
+ 'Ny2TUTPTAo5Z3n/dy09/3GeJ1RrDcdxvK++XNeFMQZesrjPbT82GG4AAAAASUVORK5CYII=\');\n'
+ ' background-position: center center;\n'
+ ' background-repeat: no-repeat;\n'
+ ' position: absolute;\n'
+ ' top: 6px;\n'
+ ' right: 6px;\n'
+ ' width: 16px !important;\n'
+ ' height: 16px;\n'
+ ' display: initial !important;\n'
+ ' padding: initial !important;\n'
+ '}\n'
+ '.toast-close:hover {\n'
+ ' background-image: url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0I'
+ 'Ars4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADQSURBVDhPnZJLCoUwDEW7a0VRd+Bn6FDQkRvwswy34j'
+ 'zvnUBK0I4MRMvNPTWmDeLivm/Ztk26rpOyLDX7vpfjOLTmI4LXdUnTNJJlWTKp4bFQECHP8yTgE4/BgRbqutZCURQyz/MLGMcxrvHCB'
+ 'Po3cVkW3e08z6ixJjwME/h5E7yRt197D0xgcl4kDSCeEAnzCWQW31v1w1nX9WU0eJqmqOlw/HFUVaWwGSyB7JzjcbDbpwugz38g2JdT'
+ 'Sc0gInnJ27bVydH6MAyy7/vjkov8AHUaPPubzCdiAAAAAElFTkSuQmCC\');\n'
+ '}\n'
+ '.toast-title {\n'
+ ' margin-bottom: 5px;\n'
+ ' font-size: 13px;\n'
+ ' overflow: hidden;\n'
+ ' max-width: 233px;\n'
+ ' max-height: 40px;\n'
+ '}\n'
+ '.toast-body {\n'
+ ' font-size: 11px;\n'
+ ' max-width: 233px;\n'
+ ' max-height: 65px;\n'
+ ' overflow: hidden;\n'
+ ' overflow-wrap: break-word;\n'
+ '}\n'
+ '.toast-new {\n'
+ ' position: absolute;\n'
+ ' right: 10px;\n'
+ ' bottom: 10px;\n'
+ '}\n'
+ '\n'
+ '.swipe-toast {\n'
+ ' margin-left: 400px;\n'
+ ' opacity: 0.25;\n'
+ ' transition: 300ms cubic-bezier(1, 0, 0.5, 0.6);\n'
+ '}\n'
+ '.toast-day {\n'
+ ' display: block;\n'
+ ' height: 0;\n'
+ ' width: 200px !important;\n'
+ ' margin: 100px 0 -100px -85px;\n'
+ ' text-align: right;\n'
+ ' font-size: 30px;\n'
+ ' font-weight: 100;\n'
+ ' font-family: "Open sans", sans serif;\n'
+ ' transform: rotate(-90deg);\n'
+ ' color: rgb(235, 235, 235);\n'
+ ' padding: initial !important;\n'
+ '}\n'
+ '#toastlist > div > dd {\n'
+ ' margin-left: 40px;\n'
+ ' margin-bottom: 5px;\n'
+ '}\n'
+ '#toastlist > div {\n'
+ ' min-height: 85px;\n'
+ '}\n';
GM_addStyle(css);
$('#userinfo_minor')[0].insertBefore($(html)[0], $('#stats_menu')[0]);
// Open the menu on click and display some tasty toasts!
$('#toastmenu').click(function (e) {
$('#toastdrop, #toastdroppad').toggle();
if ($('#toastdrop').is(':visible')) {
cacheToasts(); // Refresh the cache if it might have expired so that cache.get won't fail
if (GM_getValue('notifications.hasNewToastsToRender') == 'true') {
var d = Date.now();
$('#toastlist').html(cache.get('toastlisthtml'));
console.log("toastlist render: " + (Date.now() - d) + "ms");
GM_setValue('notifications.hasNewToastsToRender', 'false');
}
// Set all toasts as read now. This way new ones since the last opening of the menu will still be shown as new, but will already be set as read in the history.
if (toasthistory.readAll() > 0) {
GM_setValue('notifications.hasNewToastsToRender', 'true');
}
cacheToasts(true, false);
// Reset the icon as well!
$('#toast-notification-icon').attr('src', toastNotificationIconRead);
$('#toastlist').scrollTop(0);
var upToast = function () {
var $this = $(this), hash = $this.attr('id').substr(5);
var toast = toasthistory.find(hash, 'all');
if (toast != null) {
toast = new Toast(toast.data);
toast.status(Toast.STATUS.READ);
toasthistory.updateToast(toast);
$this.find('img.toast-new').hide(100, function () {
this.remove();
});
// Force a cache update on a toast update!
cacheToasts(true, false);
}
};
// Remove the unread mark of a toast and open its link (if any) in the foreground
$('.toast').click(upToast);
// Prevent the scroll-click and open the link in the background instead.
// Why MouseEvent.preventDefault only prevents the scroll-click and not the opening? no idea..
$('.toast').mousedown(function (v) {
if (v.which == 2)
v.preventDefault();
});
// Remove the unread mark of a toast
$('.toast-new').click(function (v) {
upToast.call(this.parentElement);
v.stopPropagation();
});
// Delete a toast from the history
$('.toast-close').click(function (v) {
var $this = $(this), $toast = $this.parent(), hash = $toast.attr('id').substr(5);
toasthistory.delete(hash);
$toast.addClass('swipe-toast');
window.setTimeout(function () {
$toast.slideUp(150, function () {
if ($toast.siblings().length === 0)
$toast.parent().parent().hide(100, function () {
$toast.parent().parent().remove();
});
else
$toast.remove();
});
}, 300);
v.stopPropagation();
});
}
// If we don't stop propagation the toast menu will close with every click inside it
e.stopPropagation();
});
// If we don't stop propagation the toast menu will close with every click inside it
$('#toastdrop').click(function (e) {
e.stopPropagation();
});
}
// Change the dates of the toasts returned by ToastHistory.getLast() to the local time
// Returns a new list of the same toasts in the same format but with updated dates
function localizeLast(last) {
if (last == null)
return [];
function toStamp (date) {
date = date.split('-');
return (date[0] * 31536000) + (date[1] * 2592000) + (date[2] * 86400);
}
function dateString (date) {
return date.getFullYear() + '-' + ('00' + (date.getMonth() + 1)).substr(-2) + '-' + ('00' + date.getDate()).substr(-2);
}
var newLast = [], newDay, dayTime, dayStamp, tock, tick;
newLast._push = function (obj) {
for (var k = 0; k < this.length; k++)
if (this[k].day === obj.day){
return (this[k].ticks = this[k].ticks.concat(obj.ticks), obj);
}
else if (toStamp(this[k].day) < toStamp(obj.day)) {
return (this.splice(k, 0, obj), obj);
}
this.push(obj);
return obj;
};
for (var l in last) {
tock = last[l];
dayTime = new Date(tock.day + 'T00:00:00.000Z');
dayTime = new Date(dayTime.getTime() + (dayTime.getTimezoneOffset() * 60e3));
dayStamp = Math.floor(dayTime.getTime() / 1e3);
newDay = { day: dateString(dayTime), ticks: [] };
for (var t in tock.ticks) {
tick = tock.ticks[t];
if (tick.time >= dayStamp + 86400) {
newLast._push(newDay);
dayTime.setTime(dayTime.getTime() + 86400e3);
dayStamp = Math.floor(dayTime.getTime() / 1e3);
newDay = { day: dateString(dayTime), ticks: [tick] }; // Timezones go from -12 to +14 hours, so this is safe to do
} else if (tick.time >= dayStamp) {
newDay.ticks.push(tick);
} else {
newLast._push(newDay);
dayTime.setTime(dayTime.getTime() - 86400e3);
dayStamp = Math.floor(dayTime.getTime() / 1e3);
newDay = { day: dateString(dayTime), ticks: [tick] }; // Timezones go from -12 to +14 hours, so this is safe to do
}
}
newLast._push(newDay);
}
delete newLast['_push'];
return newLast;
}
// Cache to be used later to store recent toasts for the toast menu, so that we don't have to fetch new ones every time we open the menu
// Since the cache is shared between pages of the same domain, we can also update them in the background in very strictly controlled time frames
function Cache () {
"use strict";
if (this.constructor != Cache) throw new TypeError("Constructor Cache requires 'new'");
/*
A dumb, simple cache, to make access to cross-pageload variables easy and manageable.
All cache times are in milliseconds.
*/
var storage = window['localStorage'];
if (storage == null) throw new TypeError("No Storage object could be acquired");
function getKeys () {
var keys = storage['cache.keys'];
try { return keys = JSON.parse(keys); }
catch (e) { return []; }
}
function addKey (key) {
var keys = getKeys(), i = keys.indexOf(key);
if (i < 0) keys.push(key);
return storage['cache.keys'] = JSON.stringify(keys);
}
function removeKey (key) {
var keys = getKeys(), i = keys.indexOf(key);
if (i > 0) keys.pop(i);
return storage['cache.keys'] = JSON.stringify(keys);
}
function pull (key) {
try {
return [true, JSON.parse(storage['cache.cache.' + key + '.value'])];
} catch (e) {
return [false];
}
}
function put (key, item) {
try {
storage['cache.cache.' + key + '.value'] = JSON.stringify(item);
return true;
} catch (e) {
return false;
}
}
this.set = function (key, obj, time) {
/*
Set a value to the given key in the cache.
Returns 'null' if an error occured and the error if succesfull.
*/
if (typeof key !== 'string' || key.length < 3 ||
typeof time !== 'number' || time < 1)
return null;
if (put(key, obj)) {
storage['cache.cache.' + key + '.time'] = Date.now() + time;
addKey(key);
return obj;
} else
return null;
};
this.get = function (key) {
/*
Retrieve a value by the given key from the cache.
Returns the value if succesfull, or 'null' if an error occured, the key doesn't exist or the value expired.
*/
if (typeof key !== 'string' || key.length < 3)
return null;
var item = pull(key);
if (item[0]) {
if (this.expires(key) > 0)
return item[1];
else // Might as well delete the expired key right away
this.del(key);
}
return null;
};
this.expires = function (key) {
/*
Get the remaining time in milliseconds before this key expires.
*/
var until = this.until(key);
if (until)
return until - Date.now();
else
return null;
};
this.until = function (key) {
/*
Get the time in milliseconds when this key expires.
*/
if (typeof key !== 'string' || key.length < 3)
return null;
return +storage['cache.cache.' + key + '.time'];
};
this.del = function (key) {
/*
Deletes a key and value from the cache.
*/
if (typeof key !== 'string' || key.length < 3)
return false;
storage.removeItem('cache.cache.' + key + '.value');
storage.removeItem('cache.cache.' + key + '.time');
removeKey(key);
return true;
};
this.storage = function () {
/*
Return the whole current cache in the following format:
{
'<key1>': {
time: <time1>,
value: <value1>
},
...
'<keyN>': {
time: <timeN>,
value: <valueN>
}
}
This will include expired keys as well!
*/
var keys = getKeys(), cache = {}, val;
for (var i in keys) {
val = pull(keys[i]);
cache[keys[i]] = {
time : this.until(keys[i]),
value : (val[0] ? val[1] : null)
};
}
return cache;
};
this.clear = function () {
/*
! DANGEROUS !
This completely empties the whole cache.
! DANGEROUS !
*/
var keys = getKeys();
for (var i in keys)
this.del(keys[i]);
return true;
}
this.cleanup = function () {
/*
Remove broken or old keys and values.
*/
var keys = getKeys(), time = Date.now();
for (var i in keys)
if (!(pull(keys[i])[0] && this.until(keys[i]) >= time)) {
this.del(keys[i]);
removeKey(keys[i]);
}
return true;
};
this.keys = function () {
/*
Return all currently cached keys, this may also include expired keys.
*/
return getKeys();
};
// Cleanup on creation, we don't want to leak storage after all.
// It is strongly suggested to keep calling Cache.cleanup() when the app is supposed to keep running for a while.
this.cleanup();
}
ex(Cache);
function ToastHistory () {
"use strict";
if (this.constructor != ToastHistory) throw new TypeError("Constructor ToastHistory requires 'new'");
/*
In the hopes of this being an efficient enough way to save possibly hundreds of toasts daily, this is the structure I came up with:
There will be one array for each day with all toasts of that day. This is to reduce access and manipulation times.
Each day array will consist of 'Tick' objects that will simply contain a hash of the concatenation of toast.title,
toast.body and toast.link to have "unique" ticks (tick.hash), a timestamp in _seconds_ (not milliseconds) (tick.time) and the data/toast (time.data).
The ticks will be pushed at the end of the days array, meaning each day goes from 0..n to oldest..newest respectively.
Additionally, an index array of all saved ticks hashes and timestamps will be kept as well for easy finding and retrieval of ticks.
And lastly another index of all histories will be kept so that we don't wind up with some forgotten history that just wastes space in memory.
An example:
'toast_history_2015-03-13': [
{
'hash'; '7737a969',
'time': 1426214125,
'data': {
'status': 0,
'type': 1,
'title': 'New post in WebM Thread',
'body': 'By Potatoe on 2015-03-11 06:23',
'link': 'https://animebytes.tv/forums.php?action=viewthread&threadid=15071&page=2#post818971'
}
},
{
'hash'; '1a9eb061',
'time': 1426214466,
'data': {
'status': 0,
'type': 2,
'title': 'New Post in a recent Thread!',
'body': 'Count to one million',
'link': 'https://animebytes.tv/forums.php?page=7741&action=viewthread&threadid=556#post819779'
}
},
{
'hash'; '6ada5d1b',
'time': 1426214967,
'data': {
'status': 0,
'type': 2,
'title': 'New Post in a recent Thread!',
'body': 'Count to one million',
'link': 'https://animebytes.tv/forums.php?page=7744&action=viewthread&threadid=556#post819936'
}
}
]
'toast_history_index_2015-03-13': [
'7737a969:1426214125',
'1a9eb061:1426214466',
'6ada5d1b:1426214967'
]
'toast_histories': [
'2015-03-13'
]
*/
this.index = -1;
this.currentDay = (new Date()).toISOString().split('T')[0]; // + 'T00:00:00.000Z'; for full parsable ISOString()
this.currentDB = [];
var loaded = false;
var histories = initGM('toast_histories', []);
initGM('toast_histories_read', []);
initGM('toast_history_index_' + this.currentDay, []);
initGM('toast_history_' + this.currentDay, []);
function Tick () {
// A single row in a dataset is called a "Tick", and includes the hash of the toast, a timestamp and the actual toasts data
// Takes either (hash, time, data) or ({hash, time, data})
if (arguments.length > 1) {
this.hash = arguments[0];
this.time = arguments[1];
this.data = arguments[2];
} else if (arguments.length == 1) {
this.hash = arguments[0].hash;
this.time = arguments[0].time;
this.data = arguments[0].data;
}
}
this.length = function () {
return (loaded ? this.currentDB.length : -1);
};
this.slice = function () {
return [].slice.apply(this.currentDB, arguments);
};
this.updateDay = function () {
// Update to the current day
this.currentDay = (new Date()).toISOString().split('T')[0]; // + 'T00:00:00.000Z'; for full parsable ISOString()
if (histories.length === 0 || histories.indexOf(this.currentDay) < 0) {
histories.push(this.currentDay);
GM_setValue('toast_histories', JSON.stringify(histories)); // Already saved in this.cleanup(), commented out for now
//this.cleanup(); // TODO: disabled to test the performance hit over time
}
};
this.load = function (day) {
// Load the current days dataset
// Has no real use except for making the iterator available
// The current dataset *should* update automatically when new toasts are pushed
this.updateDay();
this.index = -1;
if (day == null)
day = this.currentDay;
var currentDB;
try {
currentDB = JSON.parse(currentDB);
this.currentDB = currentDB;
return (loaded = true);
} catch (e) {
this.currentDB = [];
console.log("Error loading ToastDB");
console.log(e);
return (loaded = false);
}
};
this.unload = function () {
this.index = -1;
this.currentDB = [];
return !(loaded = false);
};
this.cleanup = function () {
// Deletes overly old records and updates current day
var old = new Date(), i = 0;
old = new Date(old.toISOString().split('T')[0] + 'T00:00:00.000Z');
old.setTime(old.getTime() - 604800e3); // Remove all week old records
old = old.getTime();
for (i = 0; i < histories.length; i++)
if (new Date(histories[i] + 'T00:00:00.001Z').getTime() < old)
break;
console.log("histories ", histories);
console.log("i-length ", histories.slice(i-1, histories.length));
//histories = histories.slice(i - 1, histories.length);
//GM_setValue('toast_histories', JSON.stringify(histories));
};
this.getDays = function () {
// Returns all days currently in the history
try {
return JSON.parse(GM_getValue('toast_histories'));
} catch (e) {
console.log("Error loading history DB");
console.log(e);
return [];
}
};
/* START Iterator functions */
this.hasNext = function () {
return this.index < this.currentDB.length - 1;
};
this.hasPrevious = function () {
return this.index > 0;
};
this.next = function () {
if ( !(loaded && this.hasNext()) )
return null;
this.index++;
return new Tick(this.currentDB[this.index]);
};
this.previous = function () {
if ( !(loaded && this.hasPrevious()) )
return null;
this.index--;
return new Tick(this.currentDB[this.index]);
};
this.current = function () {
if (loaded && this.currentDB.length > 0)
return new Tick(this.currentDB[this.index]);
return null;
};
this.nextToast = function () {
return new Toast(this.next().data);
};
this.previousToast = function () {
return new Toast(this.previous().data);
};
this.currentToast = function () {
return new Toast(this.current().data);
};
/* END Iterator functions */
this.find = function (hash, include) {
// Search for a toast by hash in the history
// Takes the hash and 'include', which can be either null, 'all'/true or an array of all days (as string) that should be searched
// If null, then search the current day. If 'all'/true, then search all days in the history.
// Returns the _first_ matching toast
// Returns null if no toast could be found
// I could just use this.findIndex() here, but then I would also have to parse it again, which isn't really that efficient though.
if (typeof hash == 'undefined')
return null;
if (typeof include == 'undefined')
include = [this.currentDay];
else if (include == 'all' || include === true)
include = histories;
hash = hash.toLowerCase();
var index, history;
for (var i in include) {
index = GM_getValue('toast_history_index_' + include[i]);
if (index.indexOf('"' + hash + ':') > 0) {
index = JSON.parse(index);
history = JSON.parse(GM_getValue('toast_history_' + include[i]));
for (var j=0; j<index.length; j++)
if (index[j].substr(0, 8) === hash)
return history[j];
}
}
return null;
};
this.findIndex = function (hash, include) {
// Search for a toast by hash in the history
// Takes the hash and 'include', which can be either null, 'all'/true or an array of all days (as string) that should be searched
// If null, then search the current day. If 'all'/true, then search all days in the history.
// Returns a tuple array with first the day string of the found toast and last the index of the toast in that days history
// Returned index applies to the _first_ found toast
// Returns null if no toast could be found
if (typeof hash === 'undefined' || hash == null)
return null;
if (typeof include === 'undefined' || include == null)
include = [this.currentDay];
else if (include == 'all' || include === true)
include = histories;
hash = hash.toLowerCase();
var index, history;
for (var i in include) {
index = GM_getValue('toast_history_index_' + include[i]);
if (index.indexOf('"' + hash + ':') > 0) {
index = JSON.parse(index);
history = JSON.parse(GM_getValue('toast_history_' + include[i]));
for (var j=0; j<index.length; j++)
if (index[j].substr(0, 8) === hash)
return [include[i], j];
}
}
return null;
};
this.delete = function (hash) {
// Deletes a toast from the history by hash
if (typeof hash === 'undefined' || hash == null)
return null;
var hit = this.findIndex(hash, 'all'), index, history;
if (hit) {
index = JSON.parse(GM_getValue('toast_history_index_' + hit[0]));
history = JSON.parse(GM_getValue('toast_history_' + hit[0]));
index.splice(hit[1], 1);
history.splice(hit[1], 1);
if (index.length === 0) {
this.deleteDay(hit[0]);
} else {
GM_setValue('toast_history_index_' + hit[0], JSON.stringify(index));
GM_setValue('toast_history_' + hit[0], JSON.stringify(history));
}
return true;
}
return false;
};
this.deleteDay = function (day) {
// Completely deletes a day dataset from the history
if (typeof day === 'undefined' || day == null)
return null;
for (var i in histories) {
if (histories[i] === day) {
histories.pop(i);
GM_deleteValue('toast_history_index_' + day);
GM_deleteValue('toast_history_' + day);
GM_setValue('toast_histories', JSON.stringify(histories));
return true;
}
}
return false;
};
this.getSpan = function (from, to) {
// Returns all toasts in the span between 'from' and 'to'
// Takes up to two UNIX timestamps as arguments.
// If only 'from' is provided, 'to' is now.
if (typeof from !== 'number' || (typeof to !== 'undefined' && typeof to !== 'number'))
return [];
var toN, fromN = from * 1e3;
if (to == null) {
toN = Date.now();
to = Math.floor(toN / 1e3);
} else
toN = to * 1e3;
var span = [], day, tick, dayDB, rangeDay = false, rangeTime = false, fromDate = new Date(fromN), toDate = new Date(toN),
fromDay = fromDate.toISOString().split('T')[0], toDay = toDate.toISOString().split('T')[0];
for (var d in histories) {
day = histories[d];
if (!rangeDay)
rangeDay = ((new Date(day + 'T23:59:59.999Z')).getTime() >= fromN);
if (rangeDay) {
dayDB = JSON.parse(GM_getValue('toast_history_' + day));
for (var t in dayDB) {
tick = dayDB[t];
if (!rangeTime)
rangeTime = (tick.time >= from);
if (rangeTime) {
span.push(new Tick(tick));
if (tick.time >= to)
break;
}
}
if (toDay === (new Date(day + 'T23:59:59.999Z')).getTime() >= toN);
break;
}
}
return span;
};
this.getLast = function (n) {
// Returns the last 'n' toasts from the history
if (typeof n !== 'number' || n < 1)
return [];
var day, dayDB, dayObj, last = [];
for (var i = histories.length - 1; i > -1 && n > 0; i--) {
day = histories[i];
dayDB = JSON.parse(GM_getValue('toast_history_' + day));
dayObj = {
day: day,
ticks: []
};
for (var j = dayDB.length - 1; j > -1 && n > 0; j--, n--)
dayObj.ticks.push(new Tick(dayDB[j]));
last.push(dayObj);
}
return last;
};
this.updateToast = function (toast, hash) {
// Updates a toast in the history.
// If a hash is provided, the toast at the provided hash will be replaced and the hash updated.
// Does NOT update the timestamp!
if (typeof toast === 'undefined' || toast == null)
return null;
if (typeof toast.toast !== 'undefined')
toast = toast.toast();
var newHash;
if (typeof hash === 'undefined' || hash == null)
newHash = (hash = hashCode(toast.title + toast.body + toast.link));
else
newHash = hashCode(toast.title + toast.body + toast.link);
hash = hash.toLowerCase();
var index, history;
// Most of the calls to this function will be made after 'reading' a recent notification, so searching from newest to oldest makes more sense.
for (var i = histories.length - 1; i > -1; i--) {
index = GM_getValue('toast_history_index_' + histories[i]);
if (index.indexOf('"' + hash + ':') > 0) {
index = JSON.parse(index);
history = JSON.parse(GM_getValue('toast_history_' + histories[i]));
for (var j=0; j<index.length; j++)
if (index[j].substr(0, 8) === hash) {
if (hash != newHash) {
index[j] = newHash + index[j].substr(8);
GM_setValue('toast_history_index_' + histories[i], JSON.stringify(index));
}
history[j] = new Tick(newHash, history[j].time, toast);
GM_setValue('toast_history_' + histories[i], JSON.stringify(history));
if (histories[i] === this.currentDay && j in this.currentDB && this.currentDB[j].hash === hash)
this.currentDB[j] = history[j];
return true;
}
}
}
return false;
};
this.readDay = function (day) {
// Set the STATUS flag to READ for all toasts from that day.
if (typeof day !== 'string' || day.length < 1)
return -1;
var dayDB = GM_getValue('toast_history_' + day), readcount = 0;
try {
dayDB = JSON.parse(dayDB);
} catch (e) {
return -1;
}
for (var i in dayDB) {
if (dayDB[i].data.status == Toast.STATUS.UNREAD) {
readcount++;
dayDB[i].data.status = Toast.STATUS.READ;
}
}
GM_setValue('toast_history_' + day, JSON.stringify(dayDB));
return readcount;
};
this.readAll = function () {
// Set the STATUS flag to READ for all toasts in the history!
var readDays = GM_getValue('toast_histories_read');
var date = new Date(), currentDay = date.toISOString().split('T')[0];
try {
readDays = JSON.parse(readDays);
} catch (e) {
readDays = [];
}
var unreadDays = histories.filter(function(i) {
return readDays.indexOf(i) < 0;
});
// Always "read" the current day, easier than marking the current day as unread every time a new toast pops up
if (unreadDays.indexOf(currentDay) < 0) {
unreadDays.push(currentDay);
}
var readCount = 0;
for (var i in unreadDays) {
var day = unreadDays[i];
var nrc = this.readDay(day);
if (nrc > 0) {
if (readDays.indexOf(day) < 0) {
readDays.push(day);
}
readCount += nrc;
}
}
GM_setValue('toast_histories_read', JSON.stringify(readDays));
return readCount;
}
this.push = function (toast, update) {
// Append a toast to the history
// If supplied second argument evaluates to true then an already present toast with the same hash will be updated instead of dropping it
if (typeof update === 'undefined' || update == null)
update = false;
if (typeof toast.toast === 'function')
toast = toast.toast();
var date = new Date(), timestamp = Math.floor(date.getTime() / 1e3), currentDay = date.toISOString().split('T')[0], hash = hashCode(toast.title + toast.body + toast.link);
var toastJSON = JSON.stringify(new Tick(hash, timestamp, toast));
var dayHistory = GM_getValue('toast_history_' + currentDay), dayIndex = GM_getValue('toast_history_index_' + currentDay);
// This is probably way faster then parsing the JSON first, pushing a single object and stringifying it again
// If there already exists a Tick with the exact same hash then it was probably a dupe from a race condition, so don't save it
// This could still potentially push dupes if they happened just between two UTC days, but the odds should be extremely low
if (dayHistory == null || dayHistory === '[]') {
dayHistory = '[' + toastJSON + ']';
dayIndex = '["' + hash + ':' + timestamp + '"]';
} else {
if (dayHistory.indexOf('{"hash":"' + hash + '","time":') < 0) {
if (loaded)
this.currentDB.push(new Tick(hash, timestamp, toast));
dayHistory = dayHistory.substring(0, dayHistory.length - 1) + ',' + toastJSON + ']';
dayIndex = dayIndex.substring(0, dayIndex.length - 1) + ',"' + hash + ':' + timestamp + '"]';
} else if (update)
this.updateToast(toast);
}
GM_setValue('toast_history_' + currentDay, dayHistory);
GM_setValue('toast_history_index_' + currentDay, dayIndex);
};
this.updateDay();
}
ex(ToastHistory);
function Toast () {
"use strict";
/*
Takes two possible arguments variants:
- Toast(<Object> props)
- Toast(<String|Number> type, <String|Number> status, <String> title, <String> body, <String> link)
In the first case, the object can contain ALL of the possible properties (type, status, title, body, link).
In the second case ALL 5 arguments must be set (though status can be null as well, it will default to UNREAD in that case).
*/
if (this.constructor != Toast) throw new TypeError("Constructor Toast requires 'new'");
var toast = new (function ToastObject (){})(); // The "private" actual toast object that will contain all relevant data
this.html = function (id, title) {
var icon = GM_getValue('notifyicon');
var attr = [];
attr.push('id="' + (typeof id === 'string' ? id : 'toast' + hashCode(this.title() + this.body() + this.link())) + '"');
attr.push('class="toast"');
if (typeof title === 'string')
attr.push('title="' + title + '" alt="' + title + '"');
if (typeof this.link() === 'string')
attr.push('onclick="window.open(\'' + this.link() + '\');"');
var html = '<div ' + attr.join(' ') + '>\n'
+ ' <div class="toast-iconwrapper">\n'
+ ' <img class="toast-icon" width="40px" height="40px" src="' + icon + '">\n'
+ ' </div>\n'
+ ' <span class="toast-close" alt="Delete" title="Delete Toast?"></span>\n'
+ ' <div class="toast-content">\n'
+ ' <div class="toast-title">' + this.title() + '</div>\n'
+ ' <div class="toast-body">' + this.body() + '</div>\n'
+ ' </div>\n'
+ (this.read() ? '' : ' <img src="/static/common/symbols/new.png" class="toast-new" alt="New" title="New Toast!">\n')
+ '</div>';
return html;
};
this.toJSON = function () {
return this.toast();
};
this.json = function () {
return JSON.stringify(this); // JSON.stringify calls toJSON on the supplied object as reference.
};
this.toast = function () {
return toast;
};
this.text = function () {
return this.title() + (this.status() === Toast.STATUS.UNREAD ? " -New-" : "" ) + "\n" + this.body() + "\n" + (typeof this.link() === 'string' ? this.link() : '');
};
this.read = function () {
return this.status() === Toast.STATUS.READ;
};
this.unread = function () {
return this.status() === Toast.STATUS.UNREAD;
};
this.status = function () {
if (arguments.length > 0) {
if (typeof arguments[0] === 'string' && (arguments[0] in Toast.STATUS))
return toast.status = Toast.STATUS[arguments[0]];
else if (typeof arguments[0] === 'number' && hasVal(Toast.STATUS, +arguments[0], true))
return toast.status = +arguments[0];
else
throw new TypeError("Invalid argument, expected Toast.STATUS but got '" + arguments[0] + "'");
}
return toast.status;
};
this.type = function () {
if (arguments.length > 0) {
if (typeof arguments[0] === 'string' && (arguments[0] in Toast.TYPE))
return toast.type = Toast.TYPE[arguments[0]];
else if (typeof +arguments[0] === 'number' && hasVal(Toast.TYPE, +arguments[0], true))
return toast.type = +arguments[0];
else
throw new TypeError("Invalid argument, expected Toast.TYPE but got '" + arguments[0] + "'");
}
return toast.type;
};
this.title = function () {
if (arguments.length > 0 && typeof arguments[0] === 'string')
return toast.title = arguments[0];
else if (arguments.length > 0)
throw new TypeError("Invalid argument, expected 'string' but got '" + (typeof arguments[0]) + "'");
return toast.title;
};
this.body = function () {
if (arguments.length > 0 && typeof arguments[0] === 'string')
return toast.body = arguments[0];
else if (arguments.length > 0)
throw new TypeError("Invalid argument, expected 'string' but got '" + (typeof arguments[0]) + "'");
return toast.body;
};
this.link = function () {
if (arguments.length > 0 && (typeof arguments[0] === 'string' || arguments[0] == null || arguments[0] == false))
return toast.link = arguments[0];
else if (arguments.length > 0)
throw new TypeError("Invalid argument, expected 'string' but got '" + (typeof arguments[0]) + "'");
return toast.link;
};
this.show = function (tag, history) {
var notif = new Notification(this.title(), {
'icon': GM_getValue('notifyicon'),
'body': this.body(),
'tag': tag
}), toast = this;
notif.closed = false;
notif.mclosed = false;
notif.onclick = function () {
if (typeof toast.link() === 'string')
window.open(toast.link());
this.mclose = true;
this.close();
};
notif.onclose = function () {
if (this.mclose) {
toast.status(Toast.STATUS.READ);
if (!(typeof history === 'boolean' && history === false)) {
if (typeof history === 'object' && history != null)
history.updateToast(toast.toast());
else if (typeof toasthistory !== 'undefined' && toasthistory != null)
toasthistory.updateToast(toast.toast());
}
}
this.closed = true;
};
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0)
window.setTimeout(function () {
notif.close();
}, ((notifyTimeout == 0) ? notifyInterval : notifyTimeout) * 1e3);
if (typeof CustomAudio !== 'undefined' && CustomAudio != null)
new CustomAudio(GM_getValue('notifysound')).play();
};
if (arguments.length === 1 && typeof arguments[0] === 'object') {
if (Object.keys(arguments[0]).length !== 5)
throw new TypeError("Missing properties in Object");
var props = arguments[0];
for (var key in props) {
switch (key) {
case 'type':
this.type(props[key]);
break;
case 'status':
this.status(props[key]);
break;
case 'title':
this.title(props[key]);
break;
case 'body':
this.body(props[key]);
break;
case 'link':
this.link(props[key]);
break;
default:
break;
}
}
} else if (arguments.length === 5) {
this.type(arguments[0]);
this.status(arguments[1]);
this.title(arguments[2]);
this.body(arguments[3]);
this.link(arguments[4]);
} else {
throw new TypeError("Invalid argument list");
}
if (typeof this.type() === 'undefined' || typeof this.status() === 'undefined' ||
typeof this.title() === 'undefined' || typeof this.body() === 'undefined' ||
typeof this.link() === 'undefined')
throw new TypeError("Missing properties in Object");
}
Toast.TYPE = Object.freeze({
PM: 0,
BOOKMARK: 1,
RECENT: 2,
AOTX: 3
});
Toast.STATUS = Object.freeze({
UNREAD: 0,
READ: 1
});
ex(Toast);
/*
* Initialize all needed GM or global variables
*/
// Initialize all the GM variables
var notifyInterval = initGM('notifyinterval', 300);
var notifyTimeout = initGM('notifytimeout', 15);
var oldPMs = initGM('oldpms', {});
var oldPosts = initGM('oldposts', {});
var oldRecentThreads = initGM('oldrecentthreads', {});
var gm_aotf = initGM('aotf', '', false);
var gm_aotw = initGM('aotw', '', false);
var gm_icon = initGM('notifyicon',
'data:image/png;base64,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'
, false);
var gm_sound = initGM('notifysound',
'data:audio/ogg;base64,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'
, false);
// Initialize all other global variables
var userID = document.getElementById('username_menu').firstElementChild.getAttribute('href').replace(/^\/user\/id\/([0-9]+)$/, '$1');
var toasthistory = new ToastHistory();
var cache = new Cache();
var cleanupcounter = 0;
var error = null;
/*
* All the actual "work" Functions
*/
// Actually there are already a bunch of those above, but those above are all still "beta"
// Creates a Userscript Settings Page or adds configuration options if it already exists
function createSettingsPage() {
function injectScript(content, id) {
var script = document.createElement('script');
if (id) script.setAttribute('id', id);
script.textContent = content.toString();
document.body.appendChild(script);
return script;
}
function addCheckbox(title, description, varName, onValue, offValue) {
if (typeof onValue !== "string" || typeof offValue !== "string" || onValue === offValue) { onValue='true'; offValue='false'; }
var newLi = document.createElement('li');
this[varName] = initGM(varName, onValue, false);
newLi.innerHTML =
"<span class='ue_left strong'>" + title + "</span>\n<span class='ue_right'><input type='checkbox' onvalue='" +
onValue + "' offvalue='" + offValue + "' name='" + varName + "' id='" + varName + "'" + ( (this[varName] ===
onValue) ? " checked='checked'" : " ") + ">\n<label for='" + varName + "'>" + description + "</label></span>";
newLi.addEventListener('click',
function(e){var t=e.target;if(typeof t.checked==="boolean"){if(t.checked){GM_setValue(
t.id,t.getAttribute('onvalue'));}else{GM_setValue(t.id,t.getAttribute('offvalue'));}}}
);
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addNumberInput(title, description, varName, defValue) {
if (typeof defValue !== "number") defValue = (JSON.parse(GM_getValue(varName)) || 0);
var newLi = document.createElement('li');
this[varName] = initGM(varName, defValue);
newLi.innerHTML =
"<span class='ue_left strong'>" + title + "</span>\n<span class='ue_right'><input type='number' size='50' name='" +
varName + "' id='" + varName + "' value='" + this[varName] + "'> <span>" + description + "</span></span>";
newLi.addEventListener('keypress',
function(e){var t=(e.which?e.which:e.keyCode),n=+e.target.value.replace(/(.)-/g,'$1');if(t===13&&!isNaN(n))
GM_setValue(e.target.id, JSON.stringify(n)); if (t===13||(t>31&&t!==45&&(t<48||t>57))) e.preventDefault();}
);
newLi.addEventListener('blur', function(e){var n=+e.target.value.replace(/(.)-/g,'$1');if(!isNaN(n))GM_setValue(e.target.id,JSON.stringify(n));}, true);
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addTextInput(title, description, varName, defValue) {
if (typeof defValue !== "string") defValue = (GM_getValue(varName) || "");
var newLi = document.createElement('li');
this[varName] = initGM(varName, defValue, false);
newLi.innerHTML =
"<span class='ue_left strong'>" + title + "</span>\n<span class='ue_right'><input type='text' style='margin: 0;' size=" +
"'50' name='" + varName + "' id='" + varName + "' value='" + this[varName] + "'> <span>" + description + "</span></span>";
newLi.addEventListener('keypress', function(e){var t=e.which?e.which:e.keyCode;if(t===13){GM_setValue(e.target.id,e.target.value);e.preventDefault();}});
newLi.addEventListener('blur', function(e){GM_setValue(e.target.id,e.target.value);}, true);
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addCustom(title, description) {
var newLi = document.createElement('li');
newLi.innerHTML = "<span class='ue_left strong'>"+title+"</span>\n<span class='ue_right'>"+description+"</span>";
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function relink(){$(function(){var stuff=$('#tabs > div'); $('ul.ue_tabs a').click(function(){stuff.
hide().filter(this.hash).show();$('ul.ue_tabs a').removeClass('selected');$(this).addClass('selected'
); return false; }).filter(':first,a[href="' + window.location.hash + '"]').slice(-1)[0].click(); }); }
var pose = document.createElement('div');
pose.id = "potatoes_settings";
pose.innerHTML = '<div class="head colhead_dark strong">User Script Settings</div><ul id="pose_list" class="nobullet ue_list"></ul>';
var poseanc = document.createElement('li');
poseanc.innerHTML = '&bull;<a href="#potatoes_settings">User Script Settings</a>';
var tabsNode = document.getElementById('tabs');
var linksNode = document.getElementsByClassName('ue_tabs')[0];
if (document.getElementById('potatoes_settings') == null) {
tabsNode.insertBefore(pose, tabsNode.childNodes[tabsNode.childNodes.length-2]);
linksNode.appendChild(poseanc);
document.body.removeChild(injectScript('('+relink.toString()+')();', 'settings_relink'));
}
addCheckbox("PM Notifications", "Notify about new private messages.", 'pmnotify');
addCheckbox("Bookmarks Notifications", "Notify if someone posted in a bookmarked thread.", 'bookmarknotify');
addCheckbox("AotF/AotW Notifications", "Notify if a new AotF or AotW have been chosen.", 'aotxnotify');
addCheckbox("Recent Threads Notifications", "Notify about a new post in a thread I recently posted in.", 'recentthreadnotify');
addNumberInput("Notification Check Interval", "(in seconds)", 'notifyinterval');
addNumberInput("Notification Timeout", "(in seconds) when the notifications should close automatically again.<br />0 for same as check interval, negative for indefinitely.", 'notifytimeout');
addTextInput("Notification Icon", "Any valid url or base64 encoded image.", 'notifyicon');
addTextInput("Notification Sound", "Any valid url or base64 encoded audio.<br />Check <a href='https://developer.mozill" +
"a.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility'>here</a> for supported formats.", 'notifysound');
addCustom("Test Notifications", "<a href='#' onclick='return false;' id='notifytest'>Click me!</a>");
addCustom("Reset Notifications", "Warning: this will reset all old, saved notifications. You might get spammed. <a href='#' onclick='return false;' id='notifyreset'>Reset</a>.");
document.getElementById('notifytest').addEventListener('click', function () { new Toast(0, 0, "Test Notification", "Hello World!", null).show(null, false); });
document.getElementById('notifyreset').addEventListener('click',
// This only deletes the old records that were used to check if the user has already been notified about something, NOT the toasts in the history
function(){if(confirm("Really?")){GM_deleteValue('oldpms');GM_deleteValue('oldposts');GM_deleteValue('oldrecentthreads');
GM_deleteValue('aotf');GM_deleteValue('aotw');initGM('oldpms', {});initGM('oldposts', {});initGM('oldrecentthreads', {});
initGM('aotf', '', false);initGM('aotw', '', false);new Toast(0, 0, "Notifications reset!", "All old, saved Notification"
+"s have been reset.", null).show(null, false);}}
);
}
/*
The following 'check<X>' functions are completely site-specific.
The script is currently designed to be as impartial to the site it is run on as possible,
so to migrate the script to other sites you will simply need to write a new set of site-specific funtions that only check for the data,
a safe Toast and ToastHistory and also stack working is already given by the rest of the script :)
(the only exception for 'easy migration' is the toast menu frontend which still needs some reformatting to make it easily plugable)
Just check the checkPMs function for a simple showcase on how easy managing notifications has become!
*/
// Checks 'doc' for new PMs ('doc' should be a document element of the '/inbox.php' page)
function checkPMs(doc) {
var unreadpms = [];
var newpmnode = doc.evaluate("//tr[@class='unreadpm'][1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i = 2; newpmnode != null; i++) {
unreadpms.push(newpmnode);
newpmnode = doc.evaluate("//tr[@class='unreadpm']["+i+"]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
for (var i=0; i<unreadpms.length; i++) {
var senderTd = null, sender = null, subject = null, id = null, epoch = null, link = null, toast = null;
senderTd = unreadpms[i].getElementsByTagName('td')[2];
sender = (senderTd.getElementsByTagName('a').length > 0) ? senderTd.getElementsByTagName('a')[0].innerHTML : senderTd.innerHTML;
subject = unreadpms[i].getElementsByTagName('a')[0].innerHTML;
id = unreadpms[i].getElementsByTagName('input')[0].value;
link = "/inbox.php?action=viewconv&id=" + id + "#last";
epoch = new Date(unreadpms[i].getElementsByTagName('span')[0].getAttribute('title')).getTime() / 1e3;
oldPMs = JSON.parse(GM_getValue('oldpms'));
if (oldPMs[id] !== epoch) {
// We create a new Toast object (Toast.TYPE is obviously rather site specific, a very minor thing to change though)
// Thanks to the Toast object, we don't have to be careful about losing reference to the notification or link variable when trying to close it or open the link later or something,
// that is now all handled inside the Toast object and specific to this one toast instance
toast = new Toast(Toast.TYPE.PM, Toast.STATUS.UNREAD, "New PM from " + sender, subject, link);
// We simply append it to the history, the history already takes care of dupes!
toasthistory.push(toast);
// This is site and subject specific, but in this case we should still have a minor check of dupes, so we save that we already notified about this
oldPMs[id] = epoch;
GM_setValue('oldpms', JSON.stringify(oldPMs));
// And now simply display the Toast! The Toast object creates a new Notification with all the specified data,
// a meaningful tag (first argument of Toast.show) helps to not _display_ dupes (the history takes care of dupes on a 'data' level, the tag on a 'display' level)
toast.show("pm" + id);
// We don't have to worry about anything anymore now. If a link was provided with the Toast object, then it will be opened upon clicking the Notification.
// The Toast will update its status to 'READ' in the history when we click on the Notification as well.
// The Notification will automatically close after the set timeout, not updating its status in the history this way.
document.dispatchEvent(new UserscriptEvent("toast", toast, {}));
}
}
}
// Checks 'doc' for new AotW and AotF ('doc' should be a document element of the '/index.php' page)
function checkAotx(doc) {
var aotf = null, aotftitle = null, aotfnode = doc.getElementById('aotf'), aotw = null, aotwtitle = null, aotwnode = doc.getElementById('aotw'), toast = null;
aotf = aotfnode.getElementsByTagName('a')[0].getAttribute('href');
aotw = aotwnode.getElementsByTagName('a')[0].getAttribute('href');
aotftitle = aotfnode.getElementsByTagName('img')[0].getAttribute('title');
aotwtitle = aotwnode.getElementsByTagName('img')[0].getAttribute('title');
gm_aotf = initGM('aotf', aotf, false);
gm_aotw = initGM('aotw', aotw, false);
if (aotf !== gm_aotf && aotf !== "") {
toast = new Toast(Toast.TYPE.AOTX, Toast.STATUS.UNREAD, "New Anime of the Fortnight!", aotftitle, aotf);
toasthistory.push(toast);
gm_aotf = aotf;
GM_setValue('aotf', gm_aotf);
toast.show("aotf");
document.dispatchEvent(new UserscriptEvent("toast", toast, {}));
}
if (aotw !== gm_aotw && aotw !== "") {
toast = new Toast(Toast.TYPE.AOTX, Toast.STATUS.UNREAD, "New Album of the Week!", aotwtitle, aotw);
toasthistory.push(toast);
gm_aotw = aotw;
GM_setValue('aotw', gm_aotw);
toast.show("aotw");
document.dispatchEvent(new UserscriptEvent("toast", toast, {}));
}
}
// Checks 'doc' for new Posts in bookmarked Threads ('doc' should be a document element of the '/bookmarks.php?type=3' page)
function checkBookmarks(doc) {
var row, bookmarksTable = doc.evaluate("//div[@id='content']/div[@class='thin']/table[@width='100%']", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i = bookmarksTable.rows.length - 1; row = (i > 0 ? bookmarksTable.rows[i] : null); i--) {
if (row.cells[0].getElementsByTagName('img')[0].getAttribute('class') === 'unread') {
var link = null, title = null, id = null, poster = null, date = null, epoch = null, body = null, toast = null;
link = row.cells[1].getElementsByClassName('go-last-read')[0].parentNode.getAttribute('href');
title = "New Post in " + row.cells[1].getElementsByTagName('strong')[0].textContent.trim();
id = row.cells[1].getElementsByTagName('strong')[0].getElementsByTagName('a')[0].getAttribute('href').match(/.*threadid=([0-9]+).*/)[1];
poster = row.cells[4].getElementsByTagName('a')[0].textContent;
if (poster == null) poster = row.cells[4].innerHTML.match(/By (?:<a[^>]*>)?([^<]+)(?:<\/a>)?<br>.*/)[1];
date = row.cells[4].getElementsByTagName('span')[0].getAttribute('title');
epoch = new Date(date).getTime() / 1e3;
body = "By " + poster + " on " + date;
oldPosts = JSON.parse(GM_getValue('oldposts'));
if (oldPosts[id] !== epoch) {
toast = new Toast(Toast.TYPE.BOOKMARK, Toast.STATUS.UNREAD, title, body, link);
toasthistory.push(toast);
oldPosts[id] = epoch;
GM_setValue('oldposts', JSON.stringify(oldPosts));
toast.show("post" + id);
document.dispatchEvent(new UserscriptEvent("toast", toast, {}));
}
}
}
}
// Checks 'doc' for new Posts in recent Threads ('doc' should be a document element of the '/userhistory.php?action=posts&userid=USERID' page where USERID is the Users ID)
function checkRecentThreads(doc) {
var unreadrecentthreads = [];
var newrecentthreadnode = doc.evaluate("//table[contains(@class,'forum_post')]/tbody/tr/td/span[.='(New!)'][1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i = 2; newrecentthreadnode != null; i++) {
unreadrecentthreads.push(newrecentthreadnode.parentNode);
newrecentthreadnode = doc.evaluate("//table[contains(@class,'forum_post')]/tbody/tr/td/span[.='(New!)']["+i+"]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
for (var i = 0; i < unreadrecentthreads.length; i++) {
var title = null, link = null, id = null, epoch = null, toast = null;
title = unreadrecentthreads[i].getElementsByTagName('a')[0].textContent;
link = unreadrecentthreads[i].getElementsByTagName('a')[1].getAttribute('href');
id = unreadrecentthreads[i].parentNode.parentNode.parentNode.id.replace('post', '');
epoch = new Date(unreadrecentthreads[i].getElementsByTagName('span')[0].textContent).getTime() / 1e3;
oldRecentThreads = JSON.parse(GM_getValue('oldrecentthreads'));
oldPosts = JSON.parse(GM_getValue('oldposts'));
if (oldRecentThreads[id] !== epoch && (oldPosts[id] == null || oldPosts[id] < epoch)) {
toast = new Toast(Toast.TYPE.RECENT, Toast.STATUS.UNREAD, "New Post in a recent Thread!", title, link);
toasthistory.push(toast);
oldRecentThreads[id] = epoch;
GM_setValue('oldrecentthreads', JSON.stringify(oldRecentThreads));
toast.show("post" + id);
document.dispatchEvent(new UserscriptEvent("toast", toast, {}));
}
}
}
/*
* Now put all that together and do stuff!
*/
// Add the toast menu to the header of all pages!
addExclamation();
// Add the settings to the site settings page!
if (window.location.pathname === '/user.php' && window.location.search.indexOf('action=edit') > -1)
createSettingsPage();
// Work the different checks!
window.setInterval(function () {
error = null;
// As checks we simply use an array that will be worked on element by element
// The condition will obviously be evaluated on the creation of the checks array, but that shouldn't be all too much of a problem
// The location is the page that will be fetched and given to the callback
// The callback is the function that will be called after fetching the given page, with a document of the page as first argument
// Additional, optional parameters to a check object are:
// method | The method that should be used to access the page (GET <default>, POST, etc.)
// sync | Whether the XHR should be (a)syncronous
// data | Data that should be POSTed to the given location
var checks = [
{
condition: GM_getValue('pmnotify') === 'true',
location: '/inbox.php',
callback: checkPMs
},
{
condition: GM_getValue('bookmarknotify') === 'true',
location: '/bookmarks.php?type=3',
callback: checkBookmarks
},
{
condition: GM_getValue('aotxnotify') === 'true',
location: '/index.php',
callback: checkAotx
},
{
condition: GM_getValue('recentthreadnotify') === 'true' && userID != null,
location: '/userhistory.php?action=posts&userid=' + userID,
callback: checkRecentThreads
}
];
function work(checks) {
if (typeof checks === "undefined") throw new Error("No checks to work");
var c = checks.shift();
if (typeof c !== "undefined" && c.condition)
window.setTimeout(function () {
try {
if (typeof error === "undefined" || !error) {
new XHRWrapper(c.location, c.callback, c.method, c.sync, c.data);
work(checks);
}
else {
throw error;
}
}
catch (e) {
document.dispatchEvent(new UserscriptEvent("error", null, e));
console.log("Error fetching new notifications: " + e.toString());
console.log(e);
}
}, 1e3);
}
work(checks);
}, notifyInterval * 1e3);
// Update the cached toasts in the background!
function cacheToasts(forceUpdate, forceDraw) {
if (typeof forceUpdate == 'undefined')
forceUpdate = false;
forceDraw = typeof forceDraw == 'undefined' ? 0 : (forceDraw ? 1 : -1);
var toastlist = cache.get('toastlist'), toastlisthtml = cache.get('toastlisthtml');
var doDraw = false;
if (!toastlist || !toastlisthtml || forceUpdate) {
var last = toasthistory.getLast(100);
last = localizeLast(last);
toastlist = last;
toastlisthtml = lastToHtml(last);
// Force redrawing on updated toastlist
doDraw = true;
// Cache it for half the notification interval. Should cover it well enough.
cache.set('toastlist', toastlist, (notifyInterval * 0.5) * 1e3);
cache.set('toastlisthtml', toastlisthtml, (notifyInterval * 0.66) * 1e3);
}
if (forceDraw == 1 || (forceDraw == 0 && doDraw)) {
$('#toastlist').html(toastlisthtml);
}
// Check if there are new posts!
// Only check the last day though, we don't want to annoy people with super old unread toasts.
var newt = false;
if (toastlist.length > 0) {
for (var i in toastlist[0].ticks) {
if (toastlist[0].ticks[i].data.status == Toast.STATUS.UNREAD) {
newt = true;
break;
}
}
}
if (newt) {
$('#toast-notification-icon').attr('src', toastNotificationIconUnread);
} else {
$('#toast-notification-icon').attr('src', toastNotificationIconRead);
}
if (cleanupcounter++ > 6) // Every minute
cache.cleanup();
}
cacheToasts(false, true);
window.setInterval(cacheToasts, 10e3);
document.addEventListener('toast', function (evt) {
var d = Date.now();
if (!('lastNewToastTime' in document) || document.lastNewToastTime + 1e3 > d) {
document.lastNewToastTime = d;
} else {
cacheToasts(true);
GM_setValue('notifications.hasNewToastsToRender', 'true');
}
});
ex(toasthistory, 'toasthistory');
// ==UserScript==
// @name AnimeBytes Notifications
// @author potatoe
// @version 2.6.4
// @description Shows toasty notifications for various things!
// @icon https://animebytes.tv/favicon.ico
// @include https://animebytes.tv/*
// @match https://animebytes.tv/*
// @downloadURL https://ab.nope.bz/userscripts/notifications/ab_notifications.user.js
// @updateURL https://ab.nope.bz/userscripts/notifications/ab_notifications.user.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// ==/UserScript==
/*
* Check wether the Browser supports Notifications and if they are granted for AnimeBytes
*/
if (!("Notification" in window)) alert("Notifications are not supported by your browser!");
if (Notification.permission !== "granted") document.getElementById('alerts').innerHTML += "<div id='notificationsalert' class='alertbar message' style='background: #FFB2B2; border: 1px solid #D45A5A;'><a href='javascript:Notification.requestPermission(function () {document.getElementById(\"notificationsalert\").parentNode.removeChild(document.getElementById(\"notificationsalert\"));});' style='color: #E40000;'>Enable Notifications</a></div>";
if (Notification.permission === "denied") alert("Notifications are denied for AnimeBytes, please reenable them in your browsers settings or disable/remove this userscript (AnimeBytes Notifications).");
/*
* Define some basic needed Functions and Objects
*/
// Native script support for Chrome
var notSupported = false;
try { notSupported = (this.GM_getValue.toString && this.GM_getValue.toString().indexOf("not supported")>-1); } catch (e) {}
if (!this.GM_getValue || notSupported) {
this.GM_getValue=function (key,def) { return localStorage[key] || def; };
this.GM_setValue=function (key,value) { return localStorage[key]=value; };
this.GM_deleteValue=function (key) { return delete localStorage[key]; };
}
// Simple custom NetworkError that gets thrown in case a XMLHttpRequest fails
NetworkError = function (message) {
this.name = "NetworkError";
this.message = (message || "");
};
NetworkError.prototype = Error.prototype;
// Custom UserscriptEvent that can be fired for various custom Events. Most notably error Events!
// UserscriptEvent not yet fully supported (dispatchEvent() doesn't seem to accept any other than the standard Events).
/*UserscriptEvent = function (type, source, obj) {
if (typeof type === "undefined") throw new TypeError("Failed to construct 'UserscriptEvent': An event name must be provided.");
if (typeof obj !== "undefined" && typeof obj !== "object") throw new TypeError("Failed to construct 'UserscriptEvent': Argument is not an object.");
var evt = new Event(type);
for (var k in evt) this[k] = evt[k];
for (var k in obj) this[k] = obj[k];
this.source = (source || "AnimeBytes Notifications");
};*/
UserscriptEvent = function (type, source, obj) {
if (typeof type === "undefined") throw new TypeError("Failed to construct 'UserscriptEvent': An event name must be provided.");
if (typeof obj !== "undefined" && typeof obj !== "object") throw new TypeError("Failed to construct 'UserscriptEvent': Argument is not an object.");
var evt = new CustomEvent(type);
for (var k in obj) evt[k] = obj[k];
evt.name = "UserscriptEvent";
evt.source = (source || "AnimeBytes Notifications");
return evt;
};
UserscriptEvent.prototype = CustomEvent.prototype;
// Don't you hate it to build a new XMLHttpRequest every single time you want to fetch a site?!
// Yea, I do too...
// Depends on NetworkError
function XHRWrapper(location, callback, method, sync, data) {
if (this.constructor != XHRWrapper) throw new TypeError("Constructor XHRWrapper requires 'new'");
this.name = "XHRWrapper";
this.typeOf = function () { return this.name; };
this.toString = function () { return '[object ' + this.name + ']'; };
this.valueOf = function () { return this; };
var xhr = new XMLHttpRequest();
Object.defineProperty(this, 'xmlHttpRequest', {
get: function () { return xhr; },
set: function () { return xhr; }
});
xhr.onerror = function(e) {
var netErr = new Error("A Network Error occured while loading the resource");
// Custom error code handling if client error occurs
var xhr = e.target;
if (xhr.status == 331) netErr.message += "System went to sleep/was suspended";
netErr.xmlHttpRequestProgressEvent = e;
netErr.xmlHttpRequest = xhr;
if (typeof error !== "undefined") { error = netErr; }
else { throw netErr; }
};
xhr.onreadystatechange = function(xhrpe) {
var xhr = xhrpe.target;
if (xhr.readyState == 4 && xhr.status == 200) {
var doc = document.implementation.createHTMLDocument();
doc.documentElement.innerHTML = xhr.responseText;
xhr.__callback(doc);
}
else if (xhr.readyState == 4 && xhr.status != 200) {
var netErr = new NetworkError();
// Custom error code handling if server error occurs
console.log("Error Code " + xhr.status + " (" + xhr.statusText + ")");
if (xhr.status == 331) netErr.message = "System went to sleep/was suspended";
throw netErr;
}
};
this.location = xhr.__location = location;
this.callback = xhr.__callback = callback;
xhr.open(method || 'GET', location, (typeof sync==="undefined")?true:sync);
xhr.send(data);
return this;
}
// Make it completely seemless. For this we 'cheat' a bit by simply copying all property names in a new XHR and assigning them getters/setters to our private XHR Object.
// This does have the drawback that the 'on...' handlers will be the same for both the Wrapper and XHR, but that's not a problem for this usecase.
for (var key in new XMLHttpRequest()) {
if (!(key in ['addEventListener', 'removeEventListener', 'dispatchEvent']) && key !== key.toUpperCase()) { // Those should be seperate thought! Same with constants.
Object.defineProperty(XHRWrapper.prototype, key, {
get: new Function("return this.xmlHttpRequest."+key+";"),
set: new Function('val', "return this.xmlHttpRequest."+key+" = val;")
});
}
}
// Make some noise!
function CustomAudio (src) {
this.audio = document.createElement('audio');
if (arguments[0]) this.audio.src = arguments[0];
if (Object.defineProperty) { // The proper way
Object.defineProperty(this, 'src', {
get: function(){ return this.audio.src; },
set: function(src){ if (!arguments[0]) { this.audio.removeAttribute('src'); } else { this.audio.src = arguments[0]; } return arguments[0]; }
});
} else { // The deprecated way
this.__defineGetter__('src', function(){ return this.audio.src; });
this.__defineSetter__('src', function(src){ if (!arguments[0]) { this.audio.removeAttribute('src'); } else { this.audio.src = arguments[0]; } return arguments[0]; });
}
this.play = function() { this.audio.play(); };
return this;
}
// This helps initializing all the GM variables
// Sets 'def' to 'gm' or returns 'gm's value if already set
// 'json' controlls wether the variable is/should be encoded as JSON String
// 'overwrite' controlls wether to overwrite 'gm' if it is not the same type as 'def'
function initGM(gm, def, json, overwrite) {
if (typeof def === "undefined") throw "shit";
if (typeof overwrite !== "boolean") overwrite = true;
if (typeof json !== "boolean") json = true;
var that = GM_getValue(gm);
if (that != null) {
var err = null;
try { that = ((json)?JSON.parse(that):that); }
catch (e) { if (e.message.match(/Unexpected token .*/)) err = e; }
if (!err && Object.prototype.toString.call(that) === Object.prototype.toString.call(def)) { return that; }
else if (overwrite) {
GM_setValue(gm, ((json)?JSON.stringify(def):def));
return def;
} else { if (err) { throw err; } else { return that; } }
} else {
GM_setValue(gm, ((json)?JSON.stringify(def):def));
return def;
}
}
/*
* Initialize all needed GM or global variables
*/
// Initialize all the GM variables
var notifyInterval = initGM('notifyinterval', 300);
var notifyTimeout = initGM('notifytimeout', 15);
var oldPMs = initGM('oldpms', {});
var oldPosts = initGM('oldposts', {});
var oldRecentThreads = initGM('oldrecentthreads', {});
var gm_aotf = initGM('aotf', '', false);
var gm_aotw = initGM('aotw', '', false);
var gm_icon = initGM('notifyicon', 'data:image/png;base64,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', false);
var gm_sound = initGM('notifysound', 'data:audio/ogg;base64,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', false);
// Initialize all other global variables
var userID = document.getElementById('username_menu').firstElementChild.getAttribute('href').replace(/^\/user\/id\/([0-9]+)$/, '$1');
var error = null;
/*
* All the actual "work" Functions
*/
// Creates a Userscript Settings Page or adds configuration options if it already exists
function createSettingsPage() {
function injectScript(content, id) {
var script = document.createElement('script');
if (id) script.setAttribute('id', id);
script.textContent = content.toString();
document.body.appendChild(script);
return script;
}
function addCheckbox(title, description, varName, onValue, offValue) {
if (typeof onValue !== "string" || typeof offValue !== "string" || onValue === offValue) onValue='true', offValue='false';
var newLi = document.createElement('li');
this[varName] = initGM(varName, onValue, false);
newLi.innerHTML = "<span class='ue_left strong'>"+title+"</span>\n<span class='ue_right'><input type='checkbox' onvalue='"+onValue+"' offvalue='"+offValue+"' name='"+varName+"' id='"+varName+"'"+((this[varName]===onValue)?" checked='checked'":" ")+">\n<label for='"+varName+"'>"+description+"</label></span>";
newLi.addEventListener('click', function(e){var t=e.target;if(typeof t.checked==="boolean"){if(t.checked){GM_setValue(t.id,t.getAttribute('onvalue'));}else{GM_setValue(t.id,t.getAttribute('offvalue'));}}});
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addNumberInput(title, description, varName, defValue) {
if (typeof defValue !== "number") defValue = (JSON.parse(GM_getValue(varName)) || 0);
var newLi = document.createElement('li');
this[varName] = initGM(varName, defValue);
newLi.innerHTML = "<span class='ue_left strong'>"+title+"</span>\n<span class='ue_right'><input type='number' size='50' name='"+varName+"' id='"+varName+"' value='"+this[varName]+"'> <span>"+description+"</span></span>";
newLi.addEventListener('keypress', function(e){var t=(e.which?e.which:e.keyCode),n=+e.target.value.replace(/(.)-/g,'$1');if(t===13&&!isNaN(n))GM_setValue(e.target.id,JSON.stringify(n));if(t===13||(t>31&&t!==45&&(t<48||t>57)))e.preventDefault();});
newLi.addEventListener('blur', function(e){var n=+e.target.value.replace(/(.)-/g,'$1');if(!isNaN(n))GM_setValue(e.target.id,JSON.stringify(n));}, true);
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addTextInput(title, description, varName, defValue) {
if (typeof defValue !== "string") defValue = (GM_getValue(varName) || "");
var newLi = document.createElement('li');
this[varName] = initGM(varName, defValue, false);
newLi.innerHTML = "<span class='ue_left strong'>"+title+"</span>\n<span class='ue_right'><input type='text' style='margin: 0;' size='50' name='"+varName+"' id='"+varName+"' value='"+this[varName]+"'> <span>"+description+"</span></span>";
newLi.addEventListener('keypress', function(e){var t=e.which?e.which:e.keyCode;if(t===13){GM_setValue_setValue(e.target.id,e.target.value);e.preventDefault();}});
newLi.addEventListener('blur', function(e){GM_setValue(e.target.id,e.target.value);}, true);
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function addCustom(title, description) {
var newLi = document.createElement('li');
newLi.innerHTML = "<span class='ue_left strong'>"+title+"</span>\n<span class='ue_right'>"+description+"</span>";
var poselistNode = document.getElementById('pose_list');
poselistNode.appendChild(newLi);
return newLi;
}
function relink(){$j(function(){var stuff=$j('#tabs > div');$j('ul.ue_tabs a').click(function(){stuff.hide().filter(this.hash).show();$j('ul.ue_tabs a').removeClass('selected');$j(this).addClass('selected');return false;}).filter(':first,a[href="'+window.location.hash+'"]').slice(-1)[0].click();});}
var pose = document.createElement('div');
pose.id = "potatoes_settings";
pose.innerHTML = '<div class="head colhead_dark strong">User Script Settings</div><ul id="pose_list" class="nobullet ue_list"></ul>';
var poseanc = document.createElement('li');
poseanc.innerHTML = '&bull;<a href="#potatoes_settings">User Script Settings</a>';
var tabsNode = document.getElementById('tabs');
var linksNode = document.getElementsByClassName('ue_tabs')[0];
if (document.getElementById('potatoes_settings') == null) { tabsNode.insertBefore(pose, tabsNode.childNodes[tabsNode.childNodes.length-2]); linksNode.appendChild(poseanc); document.body.removeChild(injectScript('('+relink.toString()+')();', 'settings_relink')); }
addCheckbox("PM Notifications", "Notify about new private messages.", 'pmnotify');
addCheckbox("Bookmarks Notifications", "Notify if someone posted in a bookmarked thread.", 'bookmarknotify');
addCheckbox("AotF/AotW Notifications", "Notify if a new AotF or AotW have been chosen.", 'aotxnotify');
addCheckbox("Recent Threads Notifications", "Notify about a new post in a thread I recently posted in.", 'recentthreadnotify');
addNumberInput("Notification Check Interval", "(in seconds)", 'notifyinterval');
addNumberInput("Notification Timeout", "(in seconds) when the notifications should close automatically again.<br />0 for same as check interval, negative for indefinitely.", 'notifytimeout');
addTextInput("Notification Icon", "Any valid url or base64 encoded image.", 'notifyicon');
addTextInput("Notification Sound", "Any valid url or base64 encoded audio.<br />Check <a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility'>here</a> for supported formats.", 'notifysound');
addCustom("Test Notifications", "<a href='#' onclick='return false;' id='notifytest'>Click me!</a>");
addCustom("Reset Notifications", "Warning: this will reset all old, saved notifications. You might get spammed. <a href='#' onclick='return false;' id='notifyreset'>Reset</a>.");
document.getElementById('notifytest').addEventListener('click', function(){var testNotification=new Notification("Test Notification",{icon:GM_getValue('notifyicon'),body:"Hello World!"});testNotification.onclick=function(){this.close();};var notifyTimeout=JSON.parse(GM_getValue('notifytimeout'));if(notifyTimeout>=0)window.setTimeout(function(){testNotification.close();},((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);new CustomAudio(GM_getValue('notifysound')).play();});
document.getElementById('notifyreset').addEventListener('click', function(){if(confirm("Really?")){GM_deleteValue('oldpms');GM_deleteValue('oldposts');GM_deleteValue('oldrecentthreads');GM_deleteValue('aotf');GM_deleteValue('aotw');initGM('oldpms', {});initGM('oldposts', {});initGM('oldrecentthreads', {});initGM('aotf', '', false);initGM('aotw', '', false);var resetNotification=new Notification("Notifications reset!",{icon:GM_getValue('notifyicon'),body:"All old, saved Notifications have been reset."});resetNotification.onclick=function(){this.close();};window.setTimeout(function(){resetNotification.close();},15e3);}});
}
// Checks 'doc' for new PMs ('doc' should be a document element of the '/inbox.php' page)
function checkPMs(doc) {
var unreadpms = new Array();
var newpmnode = doc.evaluate("//tr[@class='unreadpm'][1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i=2; newpmnode!=null; i++) {
unreadpms.push(newpmnode);
newpmnode = doc.evaluate("//tr[@class='unreadpm']["+i+"]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
for (var i=0; i<unreadpms.length; i++) {
var senderTd = null, sender = null, subject = null, id = null, epoch = null;
senderTd = unreadpms[i].getElementsByTagName('td')[2];
sender = (senderTd.getElementsByTagName('a').length > 0) ? senderTd.getElementsByTagName('a')[0].innerHTML : senderTd.innerHTML;
subject = unreadpms[i].getElementsByTagName('a')[0].innerHTML;
id = unreadpms[i].getElementsByTagName('input')[0].value;
epoch = new Date(unreadpms[i].getElementsByTagName('span')[0].getAttribute('title')).getTime() / 1e3;
oldPMs = JSON.parse(GM_getValue('oldpms'));
if (oldPMs[id] !== epoch) {
eval("window.pmnotif"+id+" = new Notification('New PM from "+sender+"', {icon:'"+GM_getValue('notifyicon')+"', body:'"+subject+"', tag:'pm"+id+"'})");
eval("window.pmnotif"+id+".onclick = function () { window.open('/inbox.php?action=viewconv&id="+id+"#last'); this.close(); }");
eval("window.pmnotif"+id+".onclose = function () { this.closed=true; };");
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0) window.setTimeout(eval('(function () { window.pmnotif'+id+'.close(); })'), ((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);
oldPMs[id] = epoch;
GM_setValue('oldpms', JSON.stringify(oldPMs));
new CustomAudio(GM_getValue('notifysound')).play();
}
}
// Cleanup old Notifications
for (var x in window) if (x.indexOf('pmnotif')===0) if (window[x].closed) delete window[x];
}
// Checks 'doc' for new AotW and AotF ('doc' should be a document element of the '/index.php' page)
function checkAotx(doc) {
var aotf = null, aotftitle = null, aotfnode = doc.getElementById('aotf'), aotw = null, aotwtitle = null, aotwnode = doc.getElementById('aotw');
aotf = aotfnode.getElementsByTagName('a')[0].getAttribute('href');
aotw = aotwnode.getElementsByTagName('a')[0].getAttribute('href');
aotftitle = aotfnode.getElementsByTagName('img')[0].getAttribute('title');
aotwtitle = aotwnode.getElementsByTagName('img')[0].getAttribute('title');
gm_aotf = initGM('aotf', aotf, false);
gm_aotw = initGM('aotw', aotw, false);
if (aotf !== gm_aotf && aotf !== "") {
var aotfNotification = new Notification("New Anime of the Fortnight!", {icon:GM_getValue('notifyicon'), body:aotftitle});
aotfNotification.onclick = function () { window.open(aotf); this.close(); };
aotfNotification.onclose = function () { this.closed=true; };
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0) window.setTimeout(function(){ aotfNotification.close(); }, ((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);
gm_aotf = aotf;
GM_setValue('aotf', gm_aotf);
new CustomAudio(GM_getValue('notifysound')).play();
}
if (aotw !== gm_aotw && aotw !== "") {
var aotwNotification = new Notification("New Album of the Week!", {icon:GM_getValue('notifyicon'), body:aotwtitle});
aotwNotification.onclick = function () { window.open(aotw); this.close(); };
aotwNotification.onclose = function () { this.closed=true; };
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0) window.setTimeout(function(){ aotwNotification.close(); }, ((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);
gm_aotw = aotw;
GM_setValue('aotw', gm_aotw);
new CustomAudio(GM_getValue('notifysound')).play();
}
}
// Checks 'doc' for new Posts in bookmarked Threads ('doc' should be a document element of the '/bookmarks.php?type=3' page)
function checkBookmarks(doc) {
var bookmarksTable = doc.evaluate("//div[@id='content']/div[@class='thin']/table[@width='100%']", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i=bookmarksTable.rows.length-1; row=(i>0)?bookmarksTable.rows[i]:null; i--) {
if (row.cells[0].getElementsByTagName('img')[0].getAttribute('class') === 'unread') {
var link = null, title = null, id = null, poster = null, date = null, epoch = null;
link = row.cells[1].getElementsByClassName('go-last-read')[0].parentNode.getAttribute('href');
title = row.cells[1].getElementsByTagName('strong')[0].textContent.trim();
id = row.cells[1].getElementsByTagName('strong')[0].getElementsByTagName('a')[0].getAttribute('href').match(/.*threadid=([0-9]+).*/)[1];
poster = row.cells[4].getElementsByTagName('a')[0].textContent;
if (poster == null) poster = row.cells[4].innerHTML.match(/By (?:<a[^>]*>)?([^<]+)(?:<\/a>)?<br>.*/)[1];
date = row.cells[4].getElementsByTagName('span')[0].getAttribute('title');
epoch = new Date(date).getTime() / 1e3;
oldPosts = JSON.parse(GM_getValue('oldposts'));
if (oldPosts[id] !== epoch) {
eval("window.bmnotif"+id+" = new Notification('New Post in "+title+"', {icon:'"+GM_getValue('notifyicon')+"', body:'By "+poster+" on "+date+"', tag:'post"+id+"'})");
eval("window.bmnotif"+id+".onclick = function () { window.open('"+link+"'); this.close(); }");
eval("window.bmnotif"+id+".onclose = function () { this.closed=true; };");
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0) window.setTimeout(eval('(function () { window.bmnotif'+id+'.close(); })'), ((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);
oldPosts[id] = epoch;
GM_setValue('oldposts', JSON.stringify(oldPosts));
new CustomAudio(GM_getValue('notifysound')).play();
}
}
}
// Cleanup old Notifications
for (var x in window) if (x.indexOf('bmnotif')===0) if (window[x].closed) delete window[x];
}
// Checks 'doc' for new Posts in recent Threads ('doc' should be a document element of the '/userhistory.php?action=posts&userid=USERID' page where USERID is the Users ID)
function checkRecentThreads(doc) {
var unreadrecentthreads = new Array();
var newrecentthreadnode = doc.evaluate("//table[contains(@class,'forum_post')]/tbody/tr/td/span[.='(New!)'][1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
for (var i=2; newrecentthreadnode!=null; i++) {
unreadrecentthreads.push(newrecentthreadnode.parentNode);
newrecentthreadnode = doc.evaluate("//table[contains(@class,'forum_post')]/tbody/tr/td/span[.='(New!)']["+i+"]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
for (var i=0; i<unreadrecentthreads.length; i++) {
var title = null, link = null, id = null, epoch = null;
title = unreadrecentthreads[i].getElementsByTagName('a')[0].textContent;
link = unreadrecentthreads[i].getElementsByTagName('a')[1].getAttribute('href');
id = unreadrecentthreads[i].parentNode.parentNode.parentNode.id.replace('post', '');
epoch = new Date(unreadrecentthreads[i].getElementsByTagName('span')[0].textContent).getTime() / 1e3;
oldRecentThreads = JSON.parse(GM_getValue('oldrecentthreads'));
oldPosts = JSON.parse(GM_getValue('oldposts'));
if (oldRecentThreads[id] !== epoch && (oldPosts[id] == null || oldPosts[id] < epoch)) {
eval("window.rtnotif"+id+" = new Notification('New Post in a recent Thread!', {icon:'"+GM_getValue('notifyicon')+"', body:'"+title+"', tag:'post"+id+"'})");
eval("window.rtnotif"+id+".onclick = function () { window.open('"+link+"'); this.close(); }");
eval("window.rtnotif"+id+".onclose = function () { this.closed=true; };");
var notifyTimeout = JSON.parse(GM_getValue('notifytimeout'));
if (notifyTimeout >= 0) window.setTimeout(eval('(function () { window.rtnotif'+id+'.close(); })'), ((notifyTimeout==0)?notifyInterval:notifyTimeout)*1e3);
oldRecentThreads[id] = epoch;
GM_setValue('oldrecentthreads', JSON.stringify(oldRecentThreads));
new CustomAudio(GM_getValue('notifysound')).play();
}
}
// Cleanup old Notifications
for (var x in window) if (x.indexOf('rtnotif')===0) if (window[x].closed) delete window[x];
}
/*
* Now put all that together and do stuff!
*/
if (window.location.pathname === '/user.php' && window.location.search.indexOf('action=edit') > -1) createSettingsPage();
window.setInterval(function () {
error = null;
var checks = [
{
condition: GM_getValue('pmnotify') === 'true',
location: '/inbox.php',
callback: checkPMs
},
{
condition: GM_getValue('bookmarknotify') === 'true',
location: '/bookmarks.php?type=3',
callback: checkBookmarks
},
{
condition: GM_getValue('aotxnotify') === 'true',
location: '/index.php',
callback: checkAotx
},
{
condition: GM_getValue('recentthreadnotify') === 'true' && userID != null,
location: '/userhistory.php?action=posts&userid='+userID,
callback: checkRecentThreads
}
];
function work(checks) {
if (typeof checks === "undefined") throw new Error("No checks to work");
var c = checks.shift();
if (typeof c !== "undefined" && c.condition)
window.setTimeout(function () {
try {
if (typeof error === "undefined" || !error) {
new XHRWrapper(c.location, c.callback, c.method, c.sync, c.data);
work(checks);
}
else {
throw error;
}
}
catch (e) {
document.dispatchEvent(new UserscriptEvent("error", null, e));
console.log("Error fetching new notifications: " + e.toString());
console.log(e);
}
}, 1e3);
}
work(checks);
}, notifyInterval * 1e3);
@hlfbt
Copy link
Author

hlfbt commented May 10, 2015

The added header button

The menu and toasts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment