this post was submitted on 04 Jul 2023
37 points (100.0% liked)

Lemmy Plugins and Userscripts

2145 readers
1 users here now

A general repository for user scripts and plugins used to enhance the Lemmy browsing experience.

Post (or cross-post) your favorite Lemmy enhancements here!

General posting suggestions:

Thanks!

founded 1 year ago
MODERATORS
 

Rewrote something I made for kbin to work with lemmy. Mimics some of RES' keyboard navigation functionality.

Edit: updated so that expanded images scroll into view.

Edit 2: 2023/07/04

  • added ability to open links/comments (hold shift to open in new tab, might have to disable popup blocker)
  • traversing through entries while expand was toggled on will collapse previous entry and expand current entry preview
  • handle expanding of text posts

Edit 3: 2023/07/04

  • add ability to change to next/previous page

Edit 4: 2023/07/06

  • updated scroll into view logic
  • prevent shortcut actions when modifier keys are held (ctrl+c won't load comment page anymore)
  • updated open link button to also consider images with external links
  • updated user script metadata section for compatibility per @God@sh.itjust.works
  • navigating to next/previous page while in "expand mode" will auto-expand the first post of the new page
// ==UserScript==
// @name             lemmy navigation
// @description      Lemmy hotkeys for navigating.
// @match            https://sh.itjust.works/*
// @match            https://burggit.moe/*
// @match            https://vlemmy.net/*
// @match            https://lemmy.world/*
// @match            https://lemm.ee/*
// @version          1.2
// @run-at           document-start
// ==/UserScript==

// Set selected entry colors
const backgroundColor = 'darkslategray';
const textColor = 'white';

// Set navigation keys with keycodes here: https://www.toptal.com/developers/keycode
const nextKey = 'KeyJ';
const prevKey = 'KeyK';
const expandKey = 'KeyX';
const openCommentsKey = 'KeyC';
const openLinkKey = 'Enter';
const nextPageKey = 'KeyN';
const prevPageKey = 'KeyP';


const css = [
".selected {",
"  background-color: " + backgroundColor + " !important;",
"  color: " + textColor + ";",
"}"
].join("\n");

if (typeof GM_addStyle !== "undefined") {
    GM_addStyle(css);
} else if (typeof PRO_addStyle !== "undefined") {
    PRO_addStyle(css);
} else if (typeof addStyle !== "undefined") {
    addStyle(css);
} else {
    let node = document.createElement("style");
    node.type = "text/css";
    node.appendChild(document.createTextNode(css));
    let heads = document.getElementsByTagName("head");
    if (heads.length > 0) {
        heads[0].appendChild(node);
    } else {
        // no head yet, stick it whereever
        document.documentElement.appendChild(node);
    }
}
const selectedClass = "selected";

let currentEntry;
let entries = [];
let previousUrl = "";
let expand = false;

const targetNode = document.documentElement;
const config = { childList: true, subtree: true };

const observer = new MutationObserver(() => {
    entries = document.querySelectorAll(".post-listing, .comment-node");

    if (entries.length > 0) {
        if (location.href !== previousUrl) {
            previousUrl = location.href;
            currentEntry = null;
        }
        init();
    }
});

observer.observe(targetNode, config);

function init() {
    // If jumping to comments
    if (window.location.search.includes("scrollToComments=true") &&
        entries.length > 1 &&
        (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
    ) {
        selectEntry(entries[1], true);
    }
    // If jumping to comment from anchor link
    else if (window.location.pathname.includes("/comment/") &&
            (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
    ) {
        const commentId = window.location.pathname.replace("/comment/", "");
        const anchoredEntry = document.getElementById("comment-" + commentId);

        if (anchoredEntry) {
            selectEntry(anchoredEntry, true);
        }
    }
    // If no entries yet selected, default to first
    else if (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0) {
        selectEntry(entries[0]);
        if (expand) expandEntry();
    }

    Array.from(entries).forEach(entry => {
        entry.removeEventListener("click", clickEntry, true);
        entry.addEventListener('click', clickEntry, true);
    });

    document.removeEventListener("keydown", handleKeyPress, true);
    document.addEventListener("keydown", handleKeyPress, true);
}

function handleKeyPress(event) {
    if (["TEXTAREA", "INPUT"].indexOf(event.target.tagName) > -1) {
        return;
    }

    // Ignore when modifier keys held
    if (event.altKey || event.ctrlKey || event.metaKey) {
        return;
    }

    switch (event.code) {
        case nextKey:
        case prevKey:
            let selectedEntry;

            // Next button
            if (event.code === nextKey) {
                // if shift key also pressed
                if (event.shiftKey) {
                    selectedEntry = getNextEntrySameLevel(currentEntry);
                } else {
                    selectedEntry = getNextEntry(currentEntry);
                }
            }

            // Previous button
            if (event.code === prevKey) {
                // if shift key also pressed
                if (event.shiftKey) {
                    selectedEntry = getPrevEntrySameLevel(currentEntry);
                } else {
                    selectedEntry = getPrevEntry(currentEntry);
                }
            }

            if (selectedEntry) {
                if (expand) collapseEntry();
                selectEntry(selectedEntry, true);
                if (expand) expandEntry();
            }
            break;
        case expandKey:
            toggleExpand();
            expand = isExpanded() ? true : false;
            break;
        case openCommentsKey:
            if (event.shiftKey) {
                window.open(
                    currentEntry.querySelector("a.btn[title$='Comments']").href,
                );
            } else {
                currentEntry.querySelector("a.btn[title$='Comments']").click();
            }
            break;
        case openLinkKey:
            const linkElement = currentEntry.querySelector(".col.flex-grow-0.px-0>div>a") || currentEntry.querySelector(".col.flex-grow-1>p>a");
            if (linkElement) {
                if (event.shiftKey) {
                    window.open(linkElement.href);
                } else {
                    linkElement.click();
                }
            }
            break;
        case nextPageKey:
        case prevPageKey:
            const pageButtons = Array.from(document.querySelectorAll(".paginator>button"));

            if (pageButtons) {
                const buttonText = event.code === nextPageKey ? "Next" : "Prev";
                pageButtons.find(btn => btn.innerHTML === buttonText).click();
            }
    }
}

function getNextEntry(e) {
    const currentEntryIndex = Array.from(entries).indexOf(e);
    
    if (currentEntryIndex + 1 >= entries.length) {
        return e;
    }
    
    return entries[currentEntryIndex + 1];
}

function getPrevEntry(e) {
    const currentEntryIndex = Array.from(entries).indexOf(e);
    
    if (currentEntryIndex - 1 < 0) {
        return e;
    }
    
    return entries[currentEntryIndex - 1];
}

function getNextEntrySameLevel(e) {
    const nextSibling = e.parentElement.nextElementSibling;

    if (!nextSibling || nextSibling.getElementsByTagName("article").length < 1) {
        return getNextEntry(e);
    }
    
    return nextSibling.getElementsByTagName("article")[0];
}

function getPrevEntrySameLevel(e) {
    const prevSibling = e.parentElement.previousElementSibling;

    if (!prevSibling || prevSibling.getElementsByTagName("article").length < 1) {
        return getPrevEntry(e);
    }
    
    return prevSibling.getElementsByTagName("article")[0];
}

function clickEntry(event) {
    const e = event.currentTarget;
    const target = event.target;

    // Deselect if already selected, also ignore if clicking on any link/button
    if (e === currentEntry && e.classList.contains(selectedClass) &&
        !(
            target.tagName.toLowerCase() === "button" || target.tagName.toLowerCase() === "a" ||
            target.parentElement.tagName.toLowerCase() === "button" ||
            target.parentElement.tagName.toLowerCase() === "a" ||
            target.parentElement.parentElement.tagName.toLowerCase() === "button" ||
            target.parentElement.parentElement.tagName.toLowerCase() === "a"
        )
    ) {
        e.classList.remove(selectedClass);
    } else {
        selectEntry(e);
    }
}

function selectEntry(e, scrollIntoView=false) {
    if (currentEntry) {
        currentEntry.classList.remove(selectedClass);
    }
    currentEntry = e;
    currentEntry.classList.add(selectedClass);

    if (scrollIntoView) {
        scrollIntoViewWithOffset(e, 15)
    }
}

function isExpanded() {
    if (
        currentEntry.querySelector("a.d-inline-block:not(.thumbnail)") ||
        currentEntry.querySelector("#postContent") ||
        currentEntry.querySelector(".card-body")
    ) {
        return true;
    }

    return false;
}

function toggleExpand() {
    const expandButton = currentEntry.querySelector("button[aria-label='Expand here']");
    const textExpandButton = currentEntry.querySelector(".post-title>button");

    if (expandButton) {
        expandButton.click();

        // Scroll into view if picture/text preview cut off
        const imgContainer = currentEntry.querySelector("a.d-inline-block");
        if (imgContainer) {
            // Check container positions once image is loaded
            imgContainer.querySelector("img").addEventListener("load", function() {
                scrollIntoViewWithOffset(currentEntry, 0);
            }, true);
        }
    }

    if (textExpandButton) {
        textExpandButton.click();
    }

    scrollIntoViewWithOffset(currentEntry, 0);
}

function expandEntry() {
    if (!isExpanded()) toggleExpand();
}

function collapseEntry() {
    if (isExpanded()) toggleExpand();
}

function scrollIntoViewWithOffset(e, offset) {
    if (e.getBoundingClientRect().top < 0 ||
        e.getBoundingClientRect().bottom > window.innerHeight
    ) {
        const y = e.getBoundingClientRect().top + window.pageYOffset - offset;
        window.scrollTo({
            top: y
        });
    }
}
top 16 comments
sorted by: hot top controversial new old
[–] god@sh.itjust.works 4 points 1 year ago

I always find myself tapping J and K on lemmy and expecting it to work so thank you for making my muscle memory not go to waste! :D

[–] howdy@thesimplecorner.org 3 points 1 year ago* (last edited 1 year ago) (1 children)

I was playing around with your first version. Thank you. Would you be opposed to this going into my addon I'm making? Would attribute you ofcourse.

LemmyTools

[–] boobslider100@lemm.ee 2 points 1 year ago

Oh cool, I don't mind.

[–] shadshack@sh.itjust.works 3 points 1 year ago

This is great! I've been working with the code and added keys for upvote/downvote as well (it's basically the same as the Expand code, but targeting the Upvote/Downvote buttons. I also have it set so that if you vote, it automatically scrolls to the next post and maintains "expand" status.

Now I can scroll lemmy and upvote/downvote to mark posts as read with just a/z, exactly how I used to use RES keyboard shortcuts for Reddit.

Here's the code I'm using (pastebin because posting it in the comment keeps timing out...): https://pastebin.com/BTYyU17L

[–] afoutopatisa@lemmy.world 3 points 1 year ago (1 children)

Is this GPL? I took the liberty to fork it, to change into arrow navigation and change the styling. I also added upvote/downvote keys, before seeing shadshack did the same 🤣. Hope it's ok!

[–] boobslider100@lemm.ee 3 points 1 year ago

Oh yea go for it. I should've put it on github in the first place but I'm lazy.

[–] rbits@lemmy.fmhy.ml 2 points 1 year ago

Would you be able to add upvote and downvote buttons? Also could you make collapse work on comments? Thanks for the script! It's great!

Ooooh very nice! downloaded and installed

[–] god@sh.itjust.works 2 points 1 year ago (1 children)

had to swap the metadata section for the following for it to not throw errors on Tampermonkey on Firefox:

// ==UserScript==
// @name             lemmy navigation
// @description      Lemmy hotkeys for navigating.
// @match            https://sh.itjust.works/*
// @match            https://burggit.moe/*
// @match            https://vlemmy.net/*
// @match            https://lemmy.world/*
// @match            https://lemm.ee/*
// @version          1.1
// @run-at           document-start
// ==/UserScript==
[–] boobslider100@lemm.ee 3 points 1 year ago

Snuck it into the post. Thanks.

[–] 3v1n0@feddit.it 1 points 1 year ago (1 children)

Love it, I'd like to get also c to open comments or l / Return to open the selected one (maybe in a new tab).

[–] boobslider100@lemm.ee 3 points 1 year ago

Updated 👍. I just did the c and enter for now.

[–] remer@lemmy.ml 1 points 1 year ago

I can't thank you enough for this boobslider100 and afoutopatisa!

[–] afoutopatisa@lemmy.world 1 points 1 year ago

Is this GPL? I took the liberty to fork it, to change into arrow navigation and change the styling. I also added upvote/downvote keys, before seeing shadshack did the same 🤣. Hope it's ok!

[–] afoutopatisa@lemmy.world 0 points 1 year ago (1 children)

Could you pls make it work with https://mlmym.org/lemmy.world/? it would be a DREAM

[–] boobslider100@lemm.ee 1 points 1 year ago

I'll try taking a look this weekend but no promises.

load more comments
view more: next ›