Can we have an official NodeBB-Stylish/userscript topic now?



  • @ChaosTheEternal it's not just "narrow window mode"... there are 3 (I'll call them narrow, medium, and full); the only one that isn't currently busted is full width mode.

    Also this only happened in the past half hour or so, and I'm hoping that it won't last long enough that I need to bother adding a CSS rule to fix it...


  • Winner of the 2016 Presidential Election

    had to change .notification-list span.text

    .notification-list span.text{
        border-left:1px solid #000 !important;
        padding-left: 3px;
        margin-left:35px !important;
        color:#FFF;
    }
    

    Added the color:#FFF because some update made the foreground color like the background color...

    Filed Under: sigh


  • I survived the hour long Uno hand

    This took for fucking ever:

    /* Fix for highlight being eye-searing on new notifications*/
    
    #menu .notification-list li a, .header .notification-list li a {
        color: white;
    }
    
    #menu .notification-list li.unread a, .header .notification-list li.unread a {
        color: black;
    }
    
    
    #menu .notification-list li.unread, .header .notification-list li.unread, .notifications-list li.unread {
    	background-color: #d3d3d3;
    }
    


  • @Yamikuronue I don't think those styles will apply to /notifications... am I correct?


  • I survived the hour long Uno hand

    @anotherusername Heh.. whoops.

    Yes, but in the wrong way?

    0_1465921511884_upload-eaa679e5-ff9e-45ce-9561-8980d8098c29

    Updated.



  • @Yamikuronue I only know because I've re-styled them because that super-pale yellow was barely noticeable on my laptop against the white theme background, and I initially missed that too.

    I actually ended up just using

    .container li.unread[data-nid] {
        background-color: #fcf8af;
    }
    

    That styled them in both places.


  • 🚽 Regular

    Is there a way to remove the massive blue glow-ey border around the last read post? I'm pretty sure this isn't one of my styles as I can't find anything that looks like it might be it. Of course changing browsers totally fucked all my comments, so it's hard to tell anymore.



  • @Erufael it's supposed to make the highlighted post easier to find.

    This is the CSS that's doing it:

    .topic .posts > li {
      -webkit-transition: box-shadow 30s;
      transition: box-shadow 30s;
      box-shadow: 0 0 0 #5bc0de;
    }
    .topic .posts > li.highlight {
      -webkit-transition: box-shadow 0s;
      transition: box-shadow 0s;
      box-shadow: 0 0 5em #5bc0de;
    }
    

    The .highlight class is automatically added by NodeBB and removed after 5 seconds -- that's part of the core. A CSS transition was used to make it last longer, because people complained that the highlight was gone by the time they got to the tab for a link opened in a new tab.

    To overrule it, you'd just need to create something more specific than .topic .posts > li.highlight that sets the box-shadow to something else (0 0 0 #5bc0de gives it a border radius of 0px, which removes it entirely -- although you might not want that -- you might still want some way to identify the highlighted post).


  • 🚽 Regular

    @anotherusername said in Can we have an official NodeBB-Stylish topic now?:

    you might still want some way to identify the highlighted post).

    Correct, ideally I'd like to find a way to perma-highlight that post in a particular color. The current thing I think makes the post look weird, since there really is no actual visual border on the left and right of posts.



  • @Erufael well, like I said, NodeBB's core adds the .highlight class and removes it after 5 seconds. You couldn't technically make a permanent highlight in pure CSS, although you could kinda fake it with a really long transition time. To make it really permanent, you'd need to use JS to hook the .highlight post and add another class to trigger your custom style.


  • 🚽 Regular

    @anotherusername Hmm. Just wish I could make this look less awkward then...
    https://imgur.com/ESSubKl

    I mean, am I the only one who doesn't like that?


  • Notification Spam Recipient

    @Erufael said in Can we have an official NodeBB-Stylish topic now?:

    @anotherusername Hmm. Just wish I could make this look less awkward then...
    https://imgur.com/ESSubKl

    I mean, am I the only one who doesn't like that?

    I think it's great personally, but then at this point of development I'd rather fix functionality before making it aesthetically pleasing.


  • area_can

    @anotherusername couldn't you keep the highlight visible forever by just setting the transition to none?



  • @bb36e no, because the class is actually being removed, which is what triggers the change, which makes it begin a 30 second transition from highlighted to not highlighted. Using none would be the default, which would be that the style goes away immediately after the class is removed.

    @Erufael well, what do you want different? You could just override it entirely with something like .highlight { box-shadow: none !important } and then add another style that you like better for the highlighted post. It just has to be a transitionable style, and you might need to add the base style for a non-highlighted post so that it has something to transition to. (If you want it to last more than 5 seconds.)



  • Color-coded comments based on upvotes / downvotes.

    0_1470390188930_Screen Shot 2016-08-05 at 11.41.50.png

    Useful for quickly catching up with long threads.

    This is mostly userscript (tampermonkey), so it doesn't fit here 100%, but what the hell.

    Userscript:

    // ==UserScript==
    // @name         TDWTF vote extender
    // @namespace    http://tampermonkey.net/
    // @version      0.1
    // @description  TDWTF, attach css classes based on number of votes
    // @author       You
    // @match        https://what.thedailywtf.com/*
    // @grant        none
    // ==/UserScript==
    
    (function($) {
    	'use strict';
    
    	var CLASSES = [
    		{ votes: -1, className: 'votes-bad' },
    		{ votes: 4, className: 'votes-meh' },
    		{ votes: 9, className: 'votes-ok' },
    		{ votes: 14, className: 'votes-good' },
    		{ votes: 1000, className: 'votes-great' }
    	];
    
    	var LOOKUP = generateLookup(-100, 100, CLASSES);
    
    	setInterval(update, 300);
    
    	update();
    
    	function update() {
    		$('li[component=post]').each(function (index, el) {
    			var voteCount = Number($('[data-votes]', el).attr('data-votes')) || 0;
    			var className = LOOKUP[voteCount];
    			var oldClassName = $(el).attr('data-votes-class');
    			if (!oldClassName || oldClassName !== className) {
    				$(el)
    					.removeClass('votes ' + oldClassName)
    					.addClass('votes ' + className)
    					.attr('data-votes-class', className);
    			}
    		});
    	}
    
    	function generateLookup(from, to, classes) {
    		var index = 0;
    		var res = {};
    		for (var i = from; i <= to; i++) {
    			if (classes[index].votes < i) {
    				index++;
    			}
    			res[i] = classes[index].className;
    		}
    		return res;
    	}
    })(jQuery);
    

    Inefficient, but there's no other way to set up the classes I need AFAIK (without having access to the server).

    Stylish part is trivial:

    .votes.votes-ok {
        background: #eeffee;
    }
    .votes.votes-good {
        background: #ddffdd;
    }
    .votes.votes-great {
        background: #ccffcc;
    }
    .votes.votes-bad {
        background: #ffeeee;
    }
    

    I guess you can also hide downvoted comments or something.


  • Winner of the 2016 Presidential Election

    @cartman82 said in Can we have an official NodeBB-Stylish/userscript topic now?:

    This is mostly userscript (tampermonkey), so it doesn't fit here 100%, but what the hell.

    Filed Under: FTFY



  • @cartman82 said in Can we have an official NodeBB-Stylish/userscript topic now?:

    setInterval(update, 300);

    You should trigger it on NodeBB's built-in events:

    $(window).on('action:topic.loaded', update);
    $(window).on('action:posts.loaded', update);
    

    A mutation observer to trap changes to the div#content element would also work. Either way it'd be more efficient than running on an interval.



  • @anotherusername does that handle upvote / downvote events?

    I basically did the simplest thing possible, tested that it's not noticably slow and called it a day.



  • @cartman82 no, in that case you'd need the mutation observer. This would:

    new MutationObserver(update)
      .observe(document.getElementById("content"),
        {attributes: true, childList: true, subtree: true});
    


  • @cartman82 actually, this would also:

    $(window).on('action:topic.loaded', update);
    $(window).on('action:posts.loaded', update);
    socket.on('event:voted', update);
    


  • @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @cartman82 no, in that case you'd need the mutation observer. This would:

    Hmm, I didn't know about this API.

    Testing it now.



  • @cartman82 actually, maybe I should warn you to make sure that your update function doesn't mutate anything if there's nothing for it to do... it's rather easy to create an infinite loop and freeze the browser if you're not careful.

    If it doesn't mutate anything when there's nothing for it to do, then it'll run once, mutate stuff, trigger the mutation observer, and run again, doing nothing the second time (and thus not triggering the mutation observer).

    Otherwise you'd need to tell the mutation observer to stop observing while you're doing stuff in your function.

    edit: after looking at your function, it looks like it doesn't change anything the second time through, since the classes already exist:

    if (!oldClassName || oldClassName !== className) {
    

    So you should be okay, in this case. It's a good thing to be aware of once you start playing with mutation observers, though.



  • @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @cartman82 actually, maybe I should warn you to make sure that your update function doesn't mutate anything if there's nothing for it to do... it's rather easy to create an infinite loop and freeze the browser if you're not careful.
    If it doesn't mutate anything when there's nothing for it to do, then it'll run once, mutate stuff, trigger the mutation observer, and run again, doing nothing the second time (and thus not triggering the mutation observer).
    Otherwise you'd need to tell the mutation observer to stop observing while you're doing stuff in your function.

    Yeah, I mutate classes if there are changes.

    I went with your second option, with connecting to nodebb events. If everything is annoyance free after a few days, I'll update the OP (or post an updated code, not sure if fbmac ruined the update privilege for us yet).



  • This post is deleted!


  • Okay, the article comments spam is back, and here's a userscript for that...

    document.head.appendChild(document.createElement("style")).innerHTML = `
    
    li.comment.spam:not(:hover) div[itemprop="text"] {
        display:none
    }
    
    li.comment.spam:not(:hover):after {
        color: red;
        content: 'Comment hidden. Mouse over to view it.';
        display: block;
        font-style: italic;
        margin-top: 3pt;
    }
    
    `;
    [...document.querySelectorAll('li.comment')].forEach(comment => {
        var text = normalize_text(comment.querySelector('div[itemprop="text"]').innerText);
        var compression = lzw_encode(text).length / text.length;
        comment.title = `Post compression score: ${+compression.toFixed(5)}`;
        if (compression < 0.4) comment.classList.add('spam');
    });
    
    // removes Zalgo and converts text to the ISO basic Latin charset
    function normalize_text(t) {
        var a = [...Array(95)].map((_, i) => String.fromCharCode(i + 32)).sort((a, b) => a.localeCompare(b));
        return t.normalize('NFKD')
            .replace(/[\u0300-\u036F]/g, '').split(/(?![\udc00-\udfff])/)
            .map(c => a.indexOf(c) >= 0 || /\s/.test(c) ? c
                 : (a[a.map(a => c.localeCompare(a)).lastIndexOf(1)] || '')[c == c.toUpperCase() ? 'toUpperCase' : 'toLowerCase']())
            .join('');
    }
    
    // very simple lzw encode function in javascript
    // based on revolunet/lzw_encoder.js on github (https://gist.github.com/revolunet/843889)
    function lzw_encode(s) {
        var data = unescape(encodeURIComponent(s)).split(""), phrase = data[0];
        for (var dict = {}, code = 256, out = [], c, i = 1; i < data.length; ++ i) {
            c = data[i];
            if (dict[phrase + c] != null) phrase += c;
            else {
                out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
                dict[phrase + c] = code ++;
                phrase = c;
            }
        }
        if (phrase) out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
        return out.map(c => String.fromCharCode(c)).join("");
    }
    

    edit: fixed the error when lzw_encode was called with an empty string. Go figure...


  • FoxDev

    @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    Okay, the article comments spam is back, and here's a userscript for that...

    document.head.appendChild(document.createElement("style")).innerHTML = `
    
    li.comment.spam:not(:hover) div[itemprop="text"] {
        display:none
    }
    
    li.comment.spam:not(:hover):after {
        color: red;
        content: 'Comment hidden. Mouse over to view it.';
        display: block;
        font-style: italic;
        margin-top: 3pt;
    }
    
    `;
    [...document.querySelectorAll('li.comment')].forEach(comment => {
        var text = normalize_text(comment.querySelector('div[itemprop="text"]').innerText);
        var compression = lzw_encode(text).length / text.length;
        comment.title = `Post compression score: ${+compression.toFixed(5)}`;
        if (compression < 0.4) comment.classList.add('spam');
    });
    
    // removes Zalgo and converts text to the ISO basic Latin charset
    function normalize_text(t) {
        var a = [...Array(95)].map((_, i) => String.fromCharCode(i + 32)).sort((a, b) => a.localeCompare(b));
        return t.normalize('NFKD')
            .replace(/[\u0300-\u036F]/g, '').split(/(?![\udc00-\udfff])/)
            .map(c => a.indexOf(c) >= 0 || /\s/.test(c) ? c
                 : (a[a.map(a => c.localeCompare(a)).lastIndexOf(1)] || '')[c == c.toUpperCase() ? 'toUpperCase' : 'toLowerCase']())
            .join('');
    }
    
    // very simple lzw encode function in javascript
    // based on revolunet/lzw_encoder.js on github (https://gist.github.com/revolunet/843889)
    function lzw_encode(s) {
        var data = unescape(encodeURIComponent(s)).split(""), phrase = data[0];
        for (var dict = {}, code = 256, out = [], c, i = 1; i < data.length; ++ i) {
            c = data[i];
            if (dict[phrase + c] != null) phrase += c;
            else {
                out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
                dict[phrase + c] = code ++;
                phrase = c;
            }
        }
        out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
        return out.map(c => String.fromCharCode(c)).join("");
    }
    

    paging @ben_lubar to implement akismet on the front page articles?



  • @accalia IIRC, Akismet did very little to actually stop the spam. I wrote essentially the same filter once previously because of the spam, when we were using Akismet. Then of course like a dummy I deleted the code when we got rid of the article commenting system and moved them all to Discourse, so now I had to re-write it. Still, it's extremely simple, and the hardest part was re-googling for a good, lightweight, simple lzw-encode function.


  • FoxDev

    @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @accalia IIRC, Akismet did very little to actually stop the spam. I wrote essentially the same filter once previously because of the spam, when we were using Akismet. Then of course like a dummy I deleted the code when we got rid of the article commenting system and moved them all to Discourse, so now I had to re-write it. Still, it's extremely simple, and the hardest part was re-googling for a good, lightweight, simple lzw-encode function.

    oh. okay....

    Paging @ben_lubar to implement your spam detection/removal function into the front page comment system?



  • @accalia pfffff... that's what @mods are for. Right, @mods?!

    Seriously though, the 40% ratio might need adjusting. I just threw a number at it and it seemed to be okay.

    Also, it does appear that the longer the text is, the better it tends to compress... I might need to adjust for that. And possibly also for whitespace.


  • Discourse touched me in a no-no place

    @anotherusername said in Can we have an official NodeBB-Stylish/userscript topic now?:

    that's what @mods are for. Right, @mods?!

    Moderating email addresses? Nah. Not in general. Meta-data with them, occasionally, maybe. On here...

    Then again, discussion of @mods here in respect of moderation of front page stuff is a tad orthogonal.

    I, for example, only have posting rights there. I cant do anything else there.



  • @anotherusername okay, after loading up a bunch of comments pages with some logging, and plotting graphs... this seems to give pretty good results. (I.e. the average score-vs-length is fairly flat: as length increases, the score isn't being penalized for that.)

    styleSheet.innerHTML += `
    
    li.comment.spam:not(:hover) div[itemprop="text"] {
        display:none
    }
    
    li.comment.spam:not(:hover):after {
        color: red;
        content: 'Comment hidden. Mouse over to view it.';
        display: block;
        font-style: italic;
        margin-top: 3pt;
    }
    
    `;
    var c = [];
    [...document.querySelectorAll('li.comment')].forEach(comment => {
        var text = normalize_text(comment.querySelector('div[itemprop="text"]').innerText).replace(/\s+/, ' ');
        var compression = lzw_encode(text).length / Math.pow(text.length, .83);
        comment.title = `Post compression score: ${+compression.toFixed(5)}`;
        if (text.length > 255 && compression < 1.5) comment.classList.add('spam');
    });
    
    // removes Zalgo and converts text to the ISO basic Latin charset
    function normalize_text(t) {
        var a = [...Array(95)].map((_, i) => String.fromCharCode(i + 32)).sort((a, b) => a.localeCompare(b));
        return t.normalize('NFKD')
            .replace(/[\u0300-\u036F]/g, '').split(/(?![\udc00-\udfff])/)
            .map(c => a.indexOf(c) >= 0 || /\s/.test(c) ? c
                 : (a[a.map(a => c.localeCompare(a)).lastIndexOf(1)] || '')[c == c.toUpperCase() ? 'toUpperCase' : 'toLowerCase']())
            .join('');
    }
    
    // very simple lzw encode function in javascript
    // based on revolunet/lzw_encoder.js on github (https://gist.github.com/revolunet/843889)
    function lzw_encode(s) {
        var data = unescape(encodeURIComponent(s)).split(""), phrase = data[0];
        for (var dict = {}, code = 256, out = [], c, i = 1; i < data.length; ++ i) {
            c = data[i];
            if (dict[phrase + c] != null) phrase += c;
            else {
                out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
                dict[phrase + c] = code ++;
                phrase = c;
            }
        }
        if (phrase) out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0));
        return out.map(c => String.fromCharCode(c)).join("");
    }
    

    edit: false (?) positive:

    0_1470865546567_Untitled.png

    I mean, I can totally see why that got flagged. And even so it's just barely under the threshold.



  • I made my userscript pull YouTube videos out of the page so if a video's playing and its parent post gets unloaded because of scrolling/navigation, the video moves down to the lower-right corner and continues going. Once it's down there, it can be paused/resumed, and if you really want it big again you can click the fullscreen toggle. There's also an X to close it.

    Originally I thought I could cleanly and neatly do this by capturing the <iframe> node as it was removed and re-introducing it into the DOM. However, it turns out that an <iframe> will always reload when you try to do this (even just moving it from one place to another within the same page), so I had to have it outside the post from the very start, and positioned absolutely based on the location of a placeholder element in the post. It's somewhat ugly from a code perspective, but it works more or less seamlessly.

    Also, since the <iframe> can't be moved without forcing it to refresh, moving it from its initial fake-inline absolute position to the lower-right fixed position has to be done purely in CSS, and I couldn't figure out how to put them in a container and have them space nicely when multiple videos were down there, because then the fake-inline absolute positioning didn't work at all. So I ended up using CSS sibling selectors to add larger and larger bottom positions so they didn't overlap (completely -- you could make them overlap more or less or not at all by changing those numbers, if you wanted), which puts an upper limit on how many videos can have unique positions defined in the CSS (I didn't want to use JS for that, partly because if one goes away all the others need to move). I decided 5 was enough.

    Anyway, without further ado...

    var s = document.createElement('script');
    s.src = "https://www.youtube.com/iframe_api";
    document.head.appendChild(s);
    unsafeWindow.eval(`${function onYouTubeIframeAPIReady() {
        console.log('YouTubeIframeAPIReady');
        
        new MutationObserver(m => {
            m.reduce((a, e) => a.concat(...e.addedNodes), []).filter(n => n.querySelector).forEach(n => {
                [n, ...n.querySelectorAll('iframe')].filter(e => e.matches('.js-lazyYT > iframe')).forEach(function (i) {
                    i = i.parentElement;
                    try {
                        var youtube_id = i.getAttribute('data-youtube-id');
                        var parameters = i.getAttribute('data-parameters');
                        var height = i.getAttribute('data-height');
                        var width = i.getAttribute('data-width');
    
                        parameters = JSON.parse(`{${parameters.replace(/&/g, ',').replace(/(\w+)=/g, '"$1":')}}`);
                        if (!('iv_load_policy' in parameters)) parameters.iv_load_policy = 3; // annotations off by default
                        if (!('rel' in parameters)) parameters.rel = 0; // related videos (after video ends) off by default
    
                        var elem_id = youtube_id + '_' + (new Date).getTime();
                        var d = document.createElement('div');
                        d.id = elem_id + '_placeholder';
                        d.style.height = height + 'px';
                        d.style.width = width + 'px';
                        i.parentElement.replaceChild(d, i);
                        
                        new MutationObserver(function () {
                            if (document.getElementById('content').contains(d)) {
                                var r = d.getBoundingClientRect();
                                p.style.top = r.top + document.documentElement.scrollTop + 'px';
                            } else {
                                this.disconnect();
                                var s = player.getPlayerState();
                                if ((s == YT.PlayerState.PLAYING || s == YT.PlayerState.BUFFERING) && !player.isMuted()) {
                                    p.classList.add('orphaned');
                                } else {
                                    p.parentElement.removeChild(p);
                                }
                            }
                        }).observe(document.getElementById('content'), {childList: true, subtree: true});
    
                        var p = document.createElement('div'), r = d.getBoundingClientRect();
                        p.classList.add('floating');
                        p.style.top = r.top + document.documentElement.scrollTop + 'px';
                        p.style.left = r.left + document.documentElement.scrollLeft + 'px';
                        p.addEventListener('click', e => {
                            // clicks on the embed are captured by it, not the iframe
                            // only the 'X' button :after pseudo-element should fire this event
                            if (e.target.classList.contains('orphaned')) {
                                e.target.style.opacity = '0';
                                e.target.addEventListener('transitionend', e => {
                                    e.target.parentNode.removeChild(e.target);
                                });
                            }
                        });
    
                        var e = document.createElement('div');
                        e.id = elem_id;
                        e.style.height = height + 'px';
                        e.style.width = width + 'px';
                        p.appendChild(e);
    
                        var c = document.getElementById('content');
                        c.parentElement.insertBefore(p, c);
    
                        var player = new YT.Player(elem_id, {
                            height: height,
                            width: width,
                            videoId: youtube_id,
                            playerVars: parameters,
                            events: {
                                onReady: function onPlayerReady(e) { e.target.playVideo(); },
                                onStateChange: function onPlayerStateChange(e) {
                                    var n = Object.keys(YT.PlayerState).filter(t => YT.PlayerState[t] == e.data)[0];
                                    p.setAttribute('data-state-name', n.toLowerCase());
                                }
                            }
                        });
                    } catch (e) {
                        console.log(e);
                    }
                });
            });
        }).observe(document.getElementById('content'), {childList: true, subtree: true});
    }}`);
    
    document.head.appendChild(document.createElement('style')).innerHTML = `
    
    .floating {
        position: absolute;
        line-height: 0;
        z-index: 1;
    }
    
    .floating.orphaned {
        position: fixed;
        transition: bottom 1s, opacity 0.2s;
        box-shadow: 0 0 15px 15px white;
        left: unset !important;
        top: unset !important;
        bottom: 15px;
        right: 15px;
        opacity: 1;
    }
    
    .floating.orphaned[data-state-name="playing"] {
        z-index: 2;
    }
    
    .floating.orphaned:after {
        content: 'X';
        position: absolute;
        background: rgba(255, 255, 255, 0.5);
        box-shadow: 0 0 5px white;
        border-radius: 0.75em;
        text-align: center;
        line-height: 1.5em;
        font-weight: bold;
        cursor: pointer;
        height: 1.5em;
        width: 1.5em;
        right: -4px;
        top: -2px;
    }
    
    .floating.orphaned:hover {
        z-index: 3;
    }
    
    .floating.orphaned iframe {
        width: 240px !important;
        height: unset !important;
    }
    
    .floating.orphaned ~ .orphaned {
        bottom: 55px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned {
        bottom: 95px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned ~ .orphaned {
        bottom: 135px !important;
    }
    
    .floating.orphaned ~ .orphaned ~ .orphaned ~ .orphaned ~ .orphaned {
        bottom: 175px !important;
    }
    
    /* 5 videos should be enough for anyone, right? */
    
    `;
    

    If the YouTubeIframeAPIReady message isn't logged, you can try removing unsafeWindow.eval(`${ and }`); from the first and last lines of that main pyramid, so that they are just:

    function onYouTubeIframeAPIReady() {
        ...
    }
    

    I had to use them to run the code within the page's context because it was getting security exceptions due to GreaseMonkey's code sandbox.

    Alternately, you could just replace unsafeWindow with window, and it should work, albeit with the unnecessary overhead of converting the function to a string and then evaling it.

    edit: Added an onStateChange event to change the data-player-state attribute of the element so it can be identified with CSS. Added a CSS rule to increase z-index of orphaned videos while they're playing, so they come in front of orphaned non-playing videos. Added default parameters to turn annotations and related videos off.



  • This generates a drop-down category menu underneath the categories icon in the navbar.

    function generateCategoryMenu(categories, level) {
        var categoryList = document.createElement('ul'), t = document.createElement('i');
        if (!level) categoryList.classList.add('categories', 'category-list-bar');
    
        for (cat of categories) {
            var el = categoryList.appendChild(document.createElement('li'));
            el.classList.add(level ? "subcategory" : "category");
            var a = el.appendChild(document.createElement('a'));
            a.classList.add('content');
            a.setAttribute('href', "/category/" + cat.slug);
            a.setAttribute('title', (t.innerHTML = cat.description, t.textContent));
            var h = document.createElement(level ? 'small' : 'h2');
            if (level) {
                var s = a.appendChild(document.createElement('span'));
                s.classList.add('fa-stack', 'fa-lg');
                var i = s.appendChild(document.createElement('i'));
                i.classList.add('fa', 'fa-circle', 'fa-stack-2x');
                i.style.color = cat.bgColor;
                i = s.appendChild(document.createElement('i'));
                i.classList.add('fa', 'fa-stack-1x', cat.icon);
                i.style.color = cat.color;
            } else {
                var d = a.appendChild(document.createElement('div'));
                d.classList.add('icon', 'pull-left');
                d.style.backgroundColor = cat.bgColor;
                d.style.color = cat.color;
                var i = d.appendChild(document.createElement('i'));
                i.classList.add('fa', 'fa-fw', cat.icon);
                h.classList.add('title')
            }
            a.appendChild(h).appendChild(document.createTextNode((t.innerHTML = cat.name, t.textContent)));
            if (cat.children.length) el.appendChild(generateCategoryMenu(cat.children, level + 1 || 1));
        }
        return categoryList;
    }
    
    socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function (err, categories) {
        if (err) {
            app.alertError(err.message);
        } else {
            var cids = [];
            categories = categories.filter(function filter(cat) {
                var i = cids.indexOf(cat.cid);
                if (i < 0) {
                    cids.push(cat.cid);
                    cat.children = cat.children.filter(filter);
                }
                return i < 0;
            });
        }
    
        var t, u;
        function hide() {
            if (u) u = clearTimeout(u);
            if (!t) t = setTimeout(function () {
                if (!u) {
                    categoryMenu.style.maxHeight = categoryMenu.style.opacity = '0';
                    var e = document.querySelector('[component="navbar/title"]');
                    if (e) e.style.maxHeight = null;
                }
                t = null;
            }, 100);
        }
        function show() {
            if (t) t = clearTimeout(t);
            if (!u) u = setTimeout(function () {
                if (!t) {
                    categoryMenu.style.maxHeight = categoryMenu.scrollHeight + 'px';
                    categoryMenu.style.opacity = '1';
                    var e = document.querySelector('[component="navbar/title"]');
                    if (e) e.style.maxHeight = '0';
                }
                u = null;
            }, 500);
        }
    
        var categoryMenu = generateCategoryMenu(categories);
        document.getElementById('header-menu').appendChild(categoryMenu);
        document.getElementById('header-menu').addEventListener('mouseleave', hide);
        document.getElementById('header-menu').addEventListener('mousemove', function (e) {
            var x = (2 * e.clientX / categoryMenu.offsetWidth - 1) * Math.PI;
            x = Math.max(0, Math.min(1, Math.sin(x / 2) * (Math.cos(x) / 8 + .625) + .5));
            categoryMenu.scrollTo(x * (categoryMenu.scrollWidth - categoryMenu.offsetWidth), 0);
    
            if (!document.querySelector('[data-original-title="Categories"]:hover')) {
                for (var p = e.target; p && p.tagName != 'A'; p = p.parentElement);
                if (!categoryMenu.contains(e.target) && p) hide();
            }
        });
        document.getElementById('header-menu').addEventListener('click', function (e) {
            for (var p = e.target; p && p.tagName != 'A'; p = p.parentElement);
            if (categoryMenu.offsetHeight > 1 && p) {
                categoryMenu.style.maxHeight = categoryMenu.style.opacity = null;
                hide();
            }
        });
    
        categoryMenu.style.maxHeight = categoryMenu.style.opacity = '0';
        categoryMenu.addEventListener('mouseleave', hide);
        categoryMenu.addEventListener('mouseenter', function () {
            if (categoryMenu.offsetHeight > 1) show();
        });
        new MutationObserver(function () {
            if (document.querySelector('[data-original-title="Categories"]:hover'))
                show();
        }).observe(document.getElementById('header-menu'), {childList: true, subtree: true});
    });
    
    document.head.appendChild(document.createElement('style')).innerHTML =
        '[data-original-title="Categories"]~.tooltip{display:none!important}.category-list-bar{width:100%;overflow:hidden;' +
        'background:#fff;position:absolute;white-space:nowrap;border-bottom:solid 1px #eee;transition:linear .25s}.categor' +
        'y-list-bar li{display:inline-block;vertical-align:top;margin:0 5px;padding:0}.categories.category-list-bar>li .co' +
        'ntent h2{margin-left:55px}.categories.category-list-bar .content .icon{margin:0;position:absolute}.category-list-' +
        'bar .category>ul{opacity:1}.category-list-bar .subcategory{padding:0;margin:0 0 0 50px;display:block;min-height:u' +
        'nset}.category-list-bar .subcategory ul{padding:0 0 0 20px}.category-list-bar .subcategory .subcategory{margin:0}';
    

    It might just end up being annoying... I'll have to see whether I like it enough to keep it around...



  • This takes the strange resize control on the composer and:

    • stretches it to 100% of the window's width, so the composer can be resized from anywhere on the edge
    • reverses the control's foreground/background colors
    • adds an ns-resize cursor style to it so it's visually obvious when you're hovering over it
    • hides the icon when you're hovering over the resize tool
    .composer .resizer {
        left: 0;
    }
    
    .composer .resizer .trigger {
        width: 100%;
        height: 20px;
        top: -24px;
        left: 0;
        margin: 0;
        padding-left: 20px;
        background: none !important;
        border: none !important;
        border-radius: 0;
        line-height: 26px;
        cursor: ns-resize;
        text-align: left;
    }
    
    .composer .resizer .trigger i {
        color: #333;
        background: rgba(255,255,255,.5);
        border-radius: 50%;
        height: 22px;
        width: 22px;
        text-align: center;
    }
    
    .composer .resizer:hover .trigger i {
        visibility: hidden;
    }
    

    (I already posted it in @blakeyrat's thread, but I think it needs to be in this one.)


  • FoxDev

    Do you want to minimise the chances of accidentally viewing NSFW images at work? Fear not: your friendly neighbourhood hedgehog is here to help! Simply add the following CSS to your Stylish, and experience a reduction in potentially embarrassing mistakes by a quintillion kwatloops! :D

    ul[data-tid="22326"] details * {
        display: none !important;
    }
    
    ul[data-tid="22326"] details:after {
        content: "[NSFW image]";
    }
    

    (Note: This is only revelant to TL3s)



  • @RaceProUK probably good to add this one, too, so that you won't see the topic in the recent/unread/category listings:

    li[data-tid="22326"] {
        display: none;
    }
    

    Prevents mis-clicking, as your styles will only hide the contents, but won't prevent those images from actually loading and possibly setting off alarms in your IT department's logs...


  • 🚽 Regular

    Sometimes I forget who wrote a long post after I scroll down.

    .posts > li[component="post"] > div.clearfix.post-header {
      position: sticky; top: 83px; pointer-events: none;
    }
    
    .posts > li[component="post"] > div.clearfix.post-header > * {
      background: #2B3E50;  /* window.getComputedStyle(document.body).backgroundColor  */
      pointer-events: auto;
    }
    

    I've noticed it disappears behind YouTube videos.
    I'm going to say it's a feature, not a bug, and I totally did it on purpose.


  • Notification Spam Recipient

    @Zecc said in Can we have an official NodeBB-Stylish/userscript topic now?:

    long

    Holy crap forget about making the window 3px wider, that window is short as frick!


  • 🚽 Regular

    @Tsaukpaetra Either that or I cropped the video to the relevant area of the screen.


  • Notification Spam Recipient

    @Zecc said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @Tsaukpaetra Either that or I cropped the video to the relevant area of the screen.

    Shhh! Don't rain my :trollface: parade!


  • 🚽 Regular

    @Zecc TODO: fix the problem where the header is in front of links right at the top of the post content and prevents clicking them.

    Example: /post/1121110

    Edit pointer-events: none; works for me.



  • @anotherusername I thought this was a good addition to my stylish setup, but now the unread count is off by one. My OCD can't take that...



  • @NedFodder bookmarklet this:

    socket.emit('topics.markAsRead', ['22326'], function () { ajaxify.refresh(); });
    


  • @RaceProUK said in Can we have an official NodeBB-Stylish/userscript topic now?:

    (Note: This is only revelant to TL3s)

    Yes, the Lounge has the official porn topic, along with blackjack and hookers. Applications are accepted on that couch in the corner of the room.


  • Winner of the 2016 Presidential Election

    @Maciejasjmj said in Can we have an official NodeBB-Stylish/userscript topic now?:

    Applications are accepted on that couch in the corner of the room.

    Can we at least choose the interviewer?



  • @asdf said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @Maciejasjmj said in Can we have an official NodeBB-Stylish/userscript topic now?:

    Applications are accepted on that couch in the corner of the room.

    Can we at least choose the interviewer?

    You can choose any interviewer you want, as long as it's @boomzilla.


    Filed under: so anyone on the forum


  • Winner of the 2016 Presidential Election

    @Maciejasjmj
    Yeah, nope, I'm gonna pass.


  • ♿ (Parody)

    @Maciejasjmj said in Can we have an official NodeBB-Stylish/userscript topic now?:

    You can choose any interviewer you want, as long as it's @boomzilla.

    Which means the interview is never going to happen.


  • Discourse touched me in a no-no place

    @NedFodder said in Can we have an official NodeBB-Stylish/userscript topic now?:

    @anotherusername I thought this was a good addition to my stylish setup, but now the unread count is off by one. My OCD can't take that...

    Ignore the topic?



  • @loopback0 I don't want to hide it all the time, just when I'm I my work computer.


Log in to reply