// ==UserScript==
// @name Split-view Macrumors Spy
// @namespace https://forums.macrumors.com/spy/
// @version 0.9.9
// @author sammich
// @match https://forums.macrumors.com/spy/
// @grant GM_xmlhttpRequest
// ==/UserScript==
/*
Doco is all at the link here:
https://github.com/sammich/macrumors-spy-mod/
Notes:
- localStorage is assumed
- CSS3 transitions are assumed (fallback is no animation)
*/
// CSS to mold/mould the page to our purposes.
var styl = document.createElement('style');
var fadeInTimeMs = 300;
styl.textContent =
"body { overflow: hidden !important; }" +
".postNew .meta:before { position: relative; bottom: 2px; content: 'new '; color: rgb(0, 136, 238); font-size: smaller; font-weight: bold; }" +
"#navigation { border-bottom: 1px solid rgb(147, 166, 194); }" +
".secondary { position: absolute !important; top: 48px; display: none; z-index: 1000; }" +
"#logo { padding: 0px 10px !important; height: 48px; line-height: 48px; background-color: transparent !important; }" +
"#logo img { position: relative; top: -2px; height: 31px; }" +
".itemCount { margin-top: 33px; }" +
"body > *, #spyContents .sectionHeaders, #header .funbox, #header .brand, #header .mobile, .itemCount .arrow, .sectionHeaders > *, .sectionHeaders .event { display: none; }" +
".titleBar, #AjaxProgress, .listBlock.event { display: none !important; }" +
"#spyContents { margin: 0; padding: 0; border-right: 0; }" +
"#spyContents .snippet { max-width: 100%; overflow: hidden; color: #999; font-family: inherit; font-style: normal; white-space: nowrap; text-overflow: ellipsis; }" +
".pageWidth { max-width: 94% !important; }" +
"#header, #header * { box-shadow: none !important; }" +
"#header .navigation { margin: 0; }" +
//"#header .secondary { opacity: 0.9; -webkit-transition: opacity 300ms ease-out; -moz-transition: opacity 300ms ease-out; -o-transition: opacity 300ms ease-out; transition: opacity 300ms ease-out; }" +
"#header .secondary:hover { opacity: 1; } " +
".mainContent { margin: 0 !important; } " +
".fade-target-300 { display: none; opacity: 0; -webkit-transition: opacity 300ms ease-out; -moz-transition: opacity 300ms ease-out; -o-transition: opacity 300ms ease-out; transition: opacity 300ms ease-out; }" +
".fade-in { opacity: 1; }" +
".fade-in-90 { opacity: 0.9; }" +
".sectionMain { border: none }" +
".discussionListItem { border-left: none !important; border-right: none !important }" +
".discussionListItem:hover { background-color: #F7FBFD; cursor: pointer; }" +
".discussionListItem .listBlock { display: block; width: 100%; box-sizing: border-box; border-right: none; " +
" vertical-align: top !important; padding: 3px 7px; }" +
".itemWrapper.firstBatch { visibility: hidden; opacity: 0; -webkit-transition: opacity 300ms ease-out; -moz-transition: opacity 300ms ease-out; -o-transition: opacity 300ms ease-out; transition: opacity 300ms ease-out; }" +
".itemWrapper.show { opacity: 1; visibility: visible; }" +
".discussionListItem .prefix { position: relative; top: -1px; padding: 0 3px; }" +
".whoWhere { padding-bottom: 0; }" +
".location .major { font-size: smaller; }" +
".listBlock.info .whoWhere { padding: 0; }" +
"@media (max-width: 610px) { .discussionListItem .listBlock { border-right: none; } }" +
"@media (max-width: 520px) { .discussionList .info > div { padding: 5px 5px 5px 8px !important; } }" +
".threadLoaded .meta:before { content: '• '; color: #04c646; }" +
".loggedInUserPost { background-color: rgb(242, 250, 237) !important; }" +
/* feint text in top corner */
".meta { position: relative; float: right; padding-right: 9px; color: #999; font-size: smaller; }" +
".meta:after { position: absolute; top: -6px; right: -2px; content: '›'; font-size: 20px; }" +
/* smaller thread title and poster username */
".whoWhere > dt > a, .whoWhere > a { font-size: 13px; } "+
"#spymod_optionsArea {padding: 2px; } #spymod_optionsArea span { margin-left: 0; color: rgb(115, 126, 136); font-size: 12px; }" +
/* split view structure and contents */
"#mainview { display: flex; display: -webkit-flex; overflow: hidden; border-top: 1px solid rgb(147, 166, 194); }" +
"#mainview .header { height: 30px; width: 100%; background-color: #c6d5e8; border-bottom: 1px solid rgb(147, 166, 194); }" +
"#spymod_col1 { width: 27%; min-width: 220px; max-width: 400px; border-right: 1px solid rgb(147, 166, 194); }" +
"#spymod_col2 { flex: auto; -webkit-flex: auto; }" +
"#spymod_col2 .header { text-align: center; line-height: 28px; }" +
"#threadselector { width: 85%; }" +
"#threadbox { position: relative; margin-left: 0px; }" +
".display-frame { width: 100%; height: 100%; border: none; position:absolute; top: 0; }" +
".offscreen { position: absolute; left: -999em; } " +
/* button */
"#spymod_col2 .header button { padding: 2px 3px; position: relative; top: 1px; }" +
/* toast */
"#newVersionMessage { position: absolute; right: 0; bottom: 0; margin: 1em; padding: 4px 7px; z-index: 10000; border: 1px solid #999; border-radius: 3px; background-color: #eee; font-size: 90%; }" +
/* no loaded frame message */
"#startermessage { padding: 5em; box-sizing: border-box; text-align: center; position: absolute; width:100%; }" +
/* frame loading thing */
"#frame-loading-message { position: absolute; top: 3em; width: 100%; z-index: 1000; text-align: center; }" +
"#frame-loading-message span { background-color:rgba(100,100,100,0.78); border-radius:1em; padding:1em; color:white; }" +
"#loader-frame { opacity: 0; }";
// inject the style tag into our host page
styl.id = 'splitviewmod';
document.body.appendChild(styl);
// hashes a string
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length == 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
};
// this function replaces the one that comes on the host page
function spyInsert() {
function modPost(post) {
var tempEl = $(post);
// grab the post info from the hidden column
var time = tempEl.find('.event .titleText dt').text(),
event = tempEl.find('.event .titleText h3').text(),
forum = tempEl.find('.location .major').text();
// get the post user so we can highlight the current user's posts
var user = tempEl.find('.location .username').text();
// if the post is a new thread, add a tag to the post
if (event === 'New Thread') {
tempEl.find('.info .whoWhere a').prepend('+ ')
}
// if it's the user highlight it with a feint green
user === window.spymod_username && tempEl.addClass('loggedInUserPost');
// add the new tag for the timestamp of the post
tempEl.find('.location .whoWhere').prepend('' + time + '');
// we don't want the initial load of posts to get this tag
if (window.spymod_insertHasRunOnce) {
// tag posts while the window isn't in focus
if (!document.hasFocus()) {
tempEl.addClass('postNew');
}
}
// don't return anything if the post isn't to be shown anyway
return tempEl;
}
// for the intial run, we want to avoid the complex timeout loop prepending multiple posts
if (!window.spymod_insertHasRunOnce) {
var fragment = spyItems.reverse().map(function (post) {
post = modPost(post);
return post ? '
' + post[0].outerHTML + '
': '';
}).join('');
// add the posts
$('#spyContents .discussionListItems').html(fragment)
setTimeout(function () {
$('#spyContents .discussionListItems').find('.itemWrapper').addClass('show');
}, 50); // add a delay as there seems to be a judder when you run it too close to page load
// intialise the posts so that it skips the normal path below
spyItems = [];
window.spymod_insertHasRunOnce = true;
}
if (spyItems.length) {
// create the fragment so we can inspect and modify it
var post = modPost(spyItems.shift());
// ignored posts will be falsey
if (post) {
// modified to use the modified HTML fragment
$('#spyContents .discussionListItems').prepend('
' + post[0].outerHTML + '
');
$('#spyContents .itemWrapper:first-child').slideDown(spyTiming / 3);
}
// truncate posts when they get to 25
if ($('#spyContents .itemWrapper').length > 25) {
var lastChild = $('#spyContents .itemWrapper:last-child');
lastChild.slideUp(spyTiming / 2, function() {
lastChild.remove();
});
}
}
$('#spyContents .postNew:not(.intented)')
.addClass('intented')
.hoverIntent(function () {
$(this).removeClass('postNew');
})
spyItems.length ? setTimeout(spyInsert, spyTiming) : (spyTiming = 2E3, setTimeout(getSpyItems, 5E3))
};
function _run_spymod() {
// get current logged in username
window.spymod_username = $('#header .accountUsername').text();
window.originalSpyUrl = window.location.href;
$('#spyContents').after(
// ignore forums input
'
' +
// self-attribution
' This mod was created by sammich. ' +
'You can read more about this mod here. ' +
// version info
'Current version: . ' +
'Update avaiilable.' +
'' +
'
'
);
}
// add the split view behaviour
function _run_buildSplitView() {
// clicking to open the menu will open the page otherwise
$('#roundups a').addClass('doNotCapture');
// repo URL
var sourceBase = 'https://github.com/sammich/macrumors-spy-mod';
// grab and save the instance of the XenForo Popup so we can reset it later
setTimeout(function() {
window.spymod_alertPopup = $('#AlertsMenu_Counter').closest('li').data('XenForo.PopupMenu');
}, 1000)
// cache the url of the window
// we use pushstate when pages are opened so we reference this when we need fixed URL requests
window.spymod_cachedWindowLoc = window.location + '';
// modify the host getSpyItems function to use the location cache
window.getSpyItems = function(){$.ajax({url:window.spymod_cachedWindowLoc+"feed?last="+spyHighestId+"&r="+Math.random(),type:"GET",success:function(a){a=$.makeArray(a.feed);$.each(a,function(a,c){$.each(c,function(a,b){0' +
'
' +
'' +
'
' +
'
' +
'
' +
' ' +
' ' +
'' +
'
' +
'
' +
'
To start, click a thread to the left.
' +
'
Loading...
' +
'' +
'' +
'
' +
'
' +
'' +
// toast-style message popup for new version alert
'
'
);
var refreshControl = $('#refreshFrame'),
popFrameControl = $('#pop-frame'),
secondary_header = $('.secondary').addClass('fade-target-300'),
frameLoadMessage = $('#frame-loading-message');
// split-view panes need this height fixing otherwise overscroll won't work properly
window.onresize = function () {
var boxTop = window.innerHeight-mainview.getBoundingClientRect().top;
window.mainview.style.height = (boxTop-1) + 'px'
window.threadbox.style.height = (boxTop-31)+ 'px'
window.postslist.style.height = (boxTop-1) + 'px'
};
// skip a beat before running an initial resize
setTimeout(function () {
window.onresize();
}, 10);
// move the spy container into our left split view pane
$('#postslist').append($('#spyContents').parent())
// move navigation menu to the top of the page
$('#header').prependTo('body').show();
// 'Forums' navtab will be active by default
$('#header .active').removeClass('active');
// move the logo into the main header because otherwise, we'd have no idea it was a MR site!
// also, it adds some colour to the page
var logo = $('#logo')
logo.find('img').removeAttr('width').removeAttr('height')
$('#header').find('ul.desktop').prepend('
'+logo[0].outerHTML+'
')
// cache the popups - these are referred to when we click inside an iFrame
window.spymod_userpopups = $('#AccountMenu').add('#AlertsMenu').add('#ConversationsMenu');
// reset the loader every so often when it's not open
setInterval(function () {
if (!$('#AlertsMenu').is(':visible')) {
window.spymod_alertPopup.resetLoader();
}
}, 5000);
// define a function to be run
function runInFrame() {
// by default, the popups will close when you click outside of them
// clicks inside an iFrame don't bubble into the parent frame
// this simply translates a click into the parent
$('body').click(function() {
window.top.spymod_userpopups.hide()
});
// on the main forum page, the responsive grid plugin doesn't kick in for some reason
// this was an interval before, which works with the commented code
// also note that this would also run on pages that don't need it
// only forums.macrumors.com seems to require this
var timer = setTimeout(function() {
try {
//uix.initFunc()
/*
if ($('#forums').hasClass('audentio_grid_running')) {
clearInterval(blah);
}
*/
} catch(e) {}
}, 50);
/*
var count = +($('#AlertsMenu_Counter .Total').text());
var topCount = +(window.top.$('#AlertsMenu_Counter .Total'));
if (isNaN(count) && !isFinite(count)) count = 0;
if (isNaN(count) && !isFinite(count)) topCount = 0;
topCount.text(count + topCount);
window.top.$('#AlertsMenu_Counter').toggleClass('Zero', count + topCount === 0);
*/
// when the inner page is loaded, get the alerts counter
// apply it to the top level spy because the alert counter isn't loaded more than
// once per page load and they can be missed
var count = $('#AlertsMenu_Counter .Total').text();
window.top.$('#AlertsMenu_Counter .Total').text(count);
window.top.$('#AlertsMenu_Counter').toggleClass('Zero', count === '0');
window.top.spymod_alertPopup.resetLoader();
}
// part of a not yet implemented feature to reload history into the select dropdown
// window.spymod_history = [];
var frame = {};
Object.defineProperty(frame, 'viewer', {
get: function() {
return $('#visible-frame')[0];
}
});
Object.defineProperty(frame, 'loader', {
get: function() {
return $('#loader-frame')[0];
}
});
frame.swap = function () {
// swap frames if swap is needed
if (frame.active !== frame.loader) {
return;
}
var v = frame.viewer,
l = frame.loader;
v.id = 'loader-frame';
l.id = 'visible-frame';
$(v).removeClass('fade-in');
setTimeout(function () {
$(v).addClass('offscreen');
frame.loader.src = '';
}, 300);
$(l).show().removeClass('offscreen').addClass('fade-target-300');
setTimeout(function () {
$(l).addClass('fade-in');
}, 10);
setTimeout(function () {
$(l).show().removeClass('fade-target-300');
}, 300);
};
// when the frame is loaded, do something...
function onFrameLoad() {
// hide the initial message
window.startermessage.style.display = 'none';
frameLoadMessage.removeClass('fade-in');
setTimeout(function () {
frameLoadMessage.hide();
}, 300);
// only available when it's loaded
refreshControl.add(popFrameControl).prop('disabled', false);
// pull the title from the inner frame and update the current option text to match it
var opt = $('#threadselector').find(':selected');
try {
window.openthread = frame.active.contentWindow.location.href;
var title = frame.active.contentDocument.title;
opt.text(title);
} catch (e) {
window.openthread = frame.active.src;
}
// push the state so we can use the browser back to view a previous thread
// this is a little buggy when hashes are followed inside the inner frame
if (!window.openthread && window.spymod_poppingStateUrl != window.openthread) {
window.history.pushState(null, null, window.openthread);
}
// try not push a state when we're going back history
// buggy
window.spymod_poppingStateUrl = null;
opt[0].origin_href = window.openthread
opt[0].threadname = title
try {
// inject some CSS into the inner page to remove headers and footers
var styl = document.createElement('style');
styl.textContent = '#header .brand, #header .navigation, .sharePage, .breadBoxBottom, .funbox, footer, .similarThreads { display:none; } body { background: none !important;}'
frame.active.contentDocument.body.appendChild(styl);
// inject a function into the page to be run
var script = document.createElement('script');
script.textContent = ';(' + runInFrame.toString() + ')()';
frame.active.contentDocument.body.appendChild(script);
} catch (e) {
// don't do anything, cross origin
}
frame.swap();
}
frame.active = null;
frame.loader.onload = onFrameLoad;
frame.viewer.onload = onFrameLoad;
// refresh the frame when the button is click, but only when the src is defined
refreshControl.click(function () {
var url = frame.viewer.getAttribute('src');
if (url) {
frameLoadMessage.show().find('span').text('Refreshing...');
setTimeout(function () {
frameLoadMessage.addClass('fade-in');
}, 10);
frame.active = frame.viewer;
frame.viewer.src = url;
}
});
// button will open the current active frame in a new tab/window
popFrameControl.click(function () {
if (frame.active && frame.active.src) {
window.open(frame.active.src, '_blank');
}
});
// handles links clicked on
function openLinkInFrame(e, el) {
e.preventDefault();
$('#messageoption').text('- switch between your opened threads -');
$('#startermessage').fadeOut()
loadUrlIntoFrame(el.href);
var threadname = el.getAttribute('title') || el.textContent;
var opt = $('')
opt[0].threadname = threadname;
opt[0].origin_href = el.href
//opt[0].origin_postnum = target.href.match(/\/(.+)\//)[1];
frameLoadMessage.find('span').text('Loading: ' + threadname || 'unknown page');
$('#threadselector').append(opt);
opt.prop('selected', true);
return false;
}
function loadUrlIntoFrame(url) {
frame.active = frame.loader;
//frame.viewer.style.display = 'block';
// can't refresh if the page hasn't loaded
refreshControl.add(popFrameControl).prop('disabled', true);
frameLoadMessage.show()
setTimeout(function () {
frameLoadMessage.addClass('fade-in');
}, 10);
window.openthread = url;
frame.loader.modTriggeredPageLoad = true;
frame.loader.src = url;
}
// click handler to intercept all links
// open them in our frame
$('body').on('click', 'a:not(.doNotCapture)', function(e) {
return openLinkInFrame(e, this);
});
$('#spyContents').off('click').on('click', '.discussionListItem, a', function(e) {
var $el = $(this);
$el.closest('.discussionListItem').addClass('threadLoaded');
var target = e.target;
if ($el.is('.discussionListItem')) {
target = $el.find('.info .whoWhere a')[0];
}
// strip the tags from the element
var target = $(target.outerHTML).find('span').remove().end()[0];
return openLinkInFrame(e, target);
});
$('#threadselector').change(function () {
var sel = $(this).find(':selected')[0];
loadUrlIntoFrame(sel.origin_href);
});
$('#header').hoverIntent(
function () {
secondary_header.show()
setTimeout(function () {
secondary_header.addClass('fade-in-90');
},10);
},
function () {
secondary_header.removeClass('fade-in-90');
setTimeout(function () {
secondary_header.hide();
}, 300);
}
);
window.onpopstate = function () {
$('#threadselector option').map(function () {
if (this.origin_href == window.location.href) {
$(this).prop('selected', true);
window.spymod_poppingStateUrl = this.origin_href;
$('#threadselector').change();
}
})
}
$('#newVersionMessage .nothanks').click(function (e) {
localStorage.setItem('spymod_ignoreVersion', localStorage.getItem('spymod_latestVersion'))
var el = $(this);
el.text('Okay...').parent().delay(1000).fadeOut();
console.log('Ignoring version ' + localStorage.getItem('spymod_ignoreVersion'));
e.preventDefault();
return false;
});
$('#newVersionMessage .versionTarget').click(function () {
var el = $(this);
el.text('Refresh after updating.');
el.parent().next().fadeOut();
setTimeout(function () {
el[0].href = window.originalSpyUrl;
});
});
}
try {
var you = document.querySelector('#header .accountUsername').textContent;
} catch (e) {}
var __hoverIntent = '!function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):jQuery&&e(jQuery)}(function(e){"use strict";var t,n,o={interval:100,sensitivity:6,timeout:0},i=0,r=function(e){t=e.pageX,n=e.pageY},u=function(e,o,i,v){return Math.sqrt((i.pX-t)*(i.pX-t)+(i.pY-n)*(i.pY-n)) currentVersion) {
console.log('version ' + versionInfo.version + ' available. You have version ' + GM_info.script.version);
var toast = document.getElementById('newVersionMessage');
toast.style.display = 'block';
toast.querySelector('.versionTarget').href = versionInfo.url;
localStorage.setItem('spymod_latestVersion', versionInfo.version);
newVersion = document.getElementById('spymod_newVersionAvailable');
newVersion.textContent = 'Update available (' + versionInfo.version + ')';
newVersion.style.display = '';
newVersion.href = versionInfo.url;
}
}
});
// do some innocent anonymous user logging
if (you) {
setTimeout(function() {
GM_xmlhttpRequest({
method: "GET",
url: 'https://pure-woodland-9816.herokuapp.com/' + you.hashCode()
});
}, 1000);
}
}