// ==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: . ' + '' + '
' + '
' ); } // 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 '
' + 'New version available. ' + '' + 'Update now!' + ' ' + 'No thanks.' + '
' ); 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); } }