lemmy-keyboard-navigation

Easily navigate Lemmy with your keyboard

当前为 2023-07-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          lemmy-keyboard-navigation
// @match         https://*/*
// @grant         none
// @version       2.0
// @author        vmavromatis
// @author        [email protected]
// @author        InfinibyteF4
// @author        aglidden
// @license       GPL3
// @icon          https://raw.githubusercontent.com/vmavromatis/Lemmy-keyboard-navigation/main/icon.png?inline=true
// @homepageURL   https://github.com/vmavromatis/Lemmy-keyboard-navigation
// @namespace     https://github.com/vmavromatis/Lemmy-keyboard-navigation
// @description   Easily navigate Lemmy with your keyboard
// @run-at        document-end
// ==/UserScript==



//isLemmySite
if (document.querySelectorAll('.lemmy-site').length >= 1){

// Vim key toggle
// Default: true
// Set to false for arrow key navigation
var vimKeyNavigation = true;

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

// Set navigation keys with keycodes here: https://www.toptal.com/developers/keycode
let nextKey = 'ArrowDown';
let prevKey = 'ArrowUp';
let nextPageKey = 'ArrowRight';
let prevPageKey = 'ArrowLeft';

if (vimKeyNavigation) {
  nextKey = 'KeyJ';
  prevKey = 'KeyK';
  nextPageKey = 'KeyL';
  prevPageKey = 'KeyH';
}

const expandKey = 'KeyX';
const openCommentsKey = 'KeyC';
const openLinkandcollapseKey = 'Enter';
const parentComment = 'KeyP';
const upvoteKey = 'KeyA';
const downvoteKey = 'KeyZ';
const replycommKey = 'KeyR';
const saveKey = 'KeyS';
const popupKey = 'KeyG';
const contextKey = 'KeyQ';
const smallerimgKey = 'Minus';
const biggerimgKey = 'Equal';
const userKey = 'KeyU';
const editKey = 'KeyE';
const inputSwitchKey = 'KeyV';

const modalCommentsKey = 'KeyC';
const modalPostsKey = 'KeyP';
const modalSubscribedKey = 'Digit1';
const modalLocalKey = 'Digit2';
const modalAllKey = 'Digit3';
const modalSavedKey = 'KeyS';
const modalFrontpageKey = 'KeyF';
const modalProfileKey = 'KeyU';
const modalInboxKey = 'KeyI';

const escapeKey = 'Escape';
let modalMode = 0;
console.log('modalMode: ' + modalMode);

// Stop arrows from moving the page if not using Vim navigation
window.addEventListener("keydown", function(e) {
  if (["ArrowUp", "ArrowDown"].indexOf(e.code) > -1 && !vimKeyNavigation) {
    e.preventDefault();
  }
}, false);

// Remove scroll animations
document.documentElement.style = "scroll-behavior: auto";

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

// dialog box
let myDialog = document.createElement("dialog");
document.body.appendChild(myDialog);
let para = document.createElement("p");
para.innerText = '--- Frontpage Sort ---\nP = posts\nC = comments\n1 = subscribed\n2 = local\n3 = all\n\n--- Everywhere Else ---\nS = saved\nF = frontpage\nU = profile\nI = inbox\nV = Toggle input style';
myDialog.appendChild(para);
let button = document.createElement("button");
button.classList.add('CLOSEBUTTON1');
button.innerHTML = 'Press ESC or G to Close';
myDialog.appendChild(button);

// Global variables
let currentEntry;
let commentBlock;
let addStyle;
let PRO_addStyle;
let entries = [];
let previousUrl = "";
let expand = false;

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";

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]);
  }

  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 || event.metaKey) {
    return;
  }

  switch (modalMode) {
    case modalMode = 0:
      switch (event.code) {
        case nextKey:
        case prevKey:
          previousKey(event);
          break;
        case upvoteKey:
          upVote();
          break;
        case downvoteKey:
          downVote();
          break;
        case expandKey:
          toggleExpand();
          expand = isExpanded() ? true : false;
          break;
        case smallerimgKey:
          imgresize(0);
          break;
        case biggerimgKey:
          imgresize(1);
          break;
        case saveKey:
          save();
          break;
        case editKey:
          edit();
          break;
        case openCommentsKey:
          comments(event);
          break;
        case popupKey:
          gotodialog(1);
          instanceanduser();
          break;
        case contextKey:
          getcontext(event);
          break;
        case replycommKey:
          if (window.location.pathname.includes("/post/")) {
              reply(event);
          } else {
            community(event);
          }
          break;
        case userKey:
          visituser(event);
          break;
        case openLinkandcollapseKey:
          if (window.location.pathname.includes("/post/")) {
            toggleExpand();
          } else {
            const linkElement = currentEntry.querySelector(".col.flex-grow-1>p>a");
            if (linkElement) {
              if (event.shiftKey) {
                window.open(linkElement.href);
              } else {
                linkElement.click();
              }
            } else {
              comments(event);
            }
          }
          break;
        case parentComment: {
          let targetBlock;
          if (currentEntry.classList.contains("ms-1")) {
            targetBlock = getPrevEntry(currentEntry);
          } else if (currentEntry.parentElement.parentElement.parentElement.nodeName === "LI") {
            targetBlock = currentEntry.parentElement.parentElement.parentElement.getElementsByTagName("article")[0];
          }
          if (targetBlock) {
            if (expand) {
              collapseEntry();
            }
            selectEntry(targetBlock, true);
            if (expand) {
              expandEntry();
            }
          }
        }
        break;
        case nextPageKey:
        case prevPageKey: {
          const pageButtons = Array.from(document.querySelectorAll(".paginator>button"));

          if (pageButtons && (document.getElementsByClassName('paginator').length > 0)) {
            const buttonText = event.code === nextPageKey ? "Next" : "Prev";
            pageButtons.find(btn => btn.innerHTML === buttonText).click();
          }
          // Jump next block of comments
          if (event.code === nextPageKey) {
            commentBlock = getNextEntrySameLevel(currentEntry);
          }
          // Jump previous block of comments
          if (event.code === prevPageKey) {
            commentBlock = getPrevEntrySameLevel(currentEntry);
          }
          if (commentBlock) {
            if (expand) {
              collapseEntry();
            }
            selectEntry(commentBlock, true);
            if (expand) {
              expandEntry();
            }
          }
        }
      }
      break;
    case modalMode = 1:
      switch (event.code) {
        case escapeKey:
          modalMode = 0;
          console.log('modalMode: ' + modalMode);
          break;
        case popupKey:
          gotodialog(0);
          break;
        case inputSwitchKey:
          vimKeyNavigation = !vimKeyNavigation;
          //Repeat definitions 
          if (vimKeyNavigation) {
            nextKey = 'KeyJ';
            prevKey = 'KeyK';
            nextPageKey = 'KeyL';
            prevPageKey = 'KeyH';
          }else{
            nextKey = 'ArrowDown';
            prevKey = 'ArrowUp';
            nextPageKey = 'ArrowRight';
            prevPageKey = 'ArrowLeft';
          }
          gotodialog(0);
          break;          
        case modalSubscribedKey:
          let subelement = document.querySelectorAll('[title="Shows the communities you\'ve subscribed to"]')[0];
          subelement.click();
          gotodialog(0);
          break;
        case modalLocalKey:
          let localelement = document.querySelectorAll('[title="Shows only local communities"]')[0];
          localelement.click();
          gotodialog(0);
          break;
        case modalAllKey:
          let allelement = document.querySelectorAll('[title="Shows all communities, including federated ones"]')[0];
          allelement.click();
          gotodialog(0);
          break;
        case modalSavedKey:
          if (window.location.pathname.includes("/u/")) {
            let savedelement = document.getElementsByClassName("btn btn-outline-secondary pointer")[3];
            if (savedelement) {
              savedelement.click();
              gotodialog(0);
            }
          } else {
            instanceanduser(2);
          }
          break;
        case modalFrontpageKey:
          frontpage();
          break;
        case modalProfileKey:
          let profileelement = document.querySelectorAll('[title="Profile"]')[0];
          if (profileelement) {
            profileelement.click();
            gotodialog(0);
          } else {
            instanceanduser(1);
          }
          break;
        case modalInboxKey:
          let notifelement = document.getElementsByClassName("nav-link d-inline-flex align-items-center d-md-inline-block")[2];
          if (notifelement) {
            notifelement.click();
            gotodialog(0);
          } else {
            console.log('Not logged in!');
          }
          break;
        case modalCommentsKey:
          let commentsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[1];
          commentsbutton.click();
          gotodialog(0);
          break;
        case modalPostsKey:
          let postsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[0];
          postsbutton.click();
          gotodialog(0);
          break;
      }
  }
}

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 previousKey(event) {
  let selectedEntry;
  // Next button
  if (event.code === nextKey) {
    if (event.shiftKey && vimKeyNavigation) {
      selectedEntry = getNextEntrySameLevel(currentEntry);

    } else {
      selectedEntry = getNextEntry(currentEntry);
    }
  }
  // Previous button
  if (event.code === prevKey) {
    if (event.shiftKey && vimKeyNavigation) {
      selectedEntry = getPrevEntrySameLevel(currentEntry);

    } else {
      selectedEntry = getPrevEntry(currentEntry);
    }
  }
  if (selectedEntry) {
    if (expand) {
      collapseEntry();
    }
    selectEntry(selectedEntry, true);
    if (expand) {
      expandEntry();
    }
  }
}

function upVote() {
  const upvoteButton = currentEntry.querySelector("button[aria-label='Upvote']");

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

function downVote() {
  const downvoteButton = currentEntry.querySelector("button[aria-label='Downvote']");

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

function gotodialog(n) {

  const closeButton = document.getElementsByClassName("CLOSEBUTTON1")[0];
  closeButton.addEventListener("click", () => {
    myDialog.close();
    modalMode = 0;
    console.log('modalMode: ' + modalMode);
  });
  if (n === 1) {
    myDialog.showModal();
    modalMode = 1;
    console.log('modalMode: ' + modalMode);
  }

  if (n === 0) {
    myDialog.close();
    modalMode = 0;
    console.log('modalMode: ' + modalMode);
  }
}

function instanceanduser(n) {
  let currentinstance = window.location.origin;
  let dropdownuser = document.getElementsByClassName("btn dropdown-toggle")[0];
  let username = dropdownuser.textContent;

  if (n === 0) {
    window.location.replace(currentinstance);
  }
  if (n === 1) {
    if (username) {
      let userlink = currentinstance + "/u/" + username;
      window.location.replace(userlink);
    } else {
      console.log('Not logged in!');
      frontpage();
    }
  }
  if (n === 2) {
    if (username) {
      let savedlink = currentinstance + "/u/" + username + "?page=1&sort=New&view=Saved";
      window.location.replace(savedlink);
    } else {
      console.log('Not logged in!');
      frontpage();
    }
  }
}

function frontpage() {
  let homeelement = document.getElementsByClassName("d-flex align-items-center navbar-brand me-md-3 active")[0];
  if (homeelement) {
    homeelement.click();
    gotodialog(0);
  } else {
    instanceanduser(0);
  }
}

function reply(event) {
  const replyButton = currentEntry.querySelector("button[data-tippy-content='reply']");

  if (replyButton) {
    event.preventDefault();
    replyButton.click();
  }
}

function community(event) {
  if (event.shiftKey) {
    window.open(
      currentEntry.querySelector("a.community-link").href,
    );
  } else {
    currentEntry.querySelector("a.community-link").click();
  }
}

function visituser(event) {
  if (event.shiftKey) {
    window.open(
      currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].href,
    );
  } else {
    currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].click();
  }
}

function comments(event) {
  if (event.shiftKey) {
    window.open(
      currentEntry.querySelector("a.btn[title*='Comment']").href,
    );
  } else {
    currentEntry.querySelector("a.btn[title*='Comment']").click();
  }
}

function getcontext(event) {
  if (event.shiftKey) {
    window.open(
      currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].href,
    );
  } else {
    currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].click();
  }
}

let maxsize = 0;
console.log('maxsize ' + maxsize);

function imgresize(n) {
  let expandedimg = currentEntry.getElementsByClassName("overflow-hidden pictrs-image img-fluid img-expanded slight-radius")[0];
  let expandedheight = expandedimg.height;
  let expandedwidth = expandedimg.width;
  let expandedheightbefore = expandedheight;
  let expandedwidthbefore = expandedwidth;

  if (n === 0) {
    expandedheight = expandedheight / 1.15;
    expandedwidth = expandedwidth / 1.15;
    expandedimg.style.height = expandedheight + 'px';
    expandedimg.style.width = expandedwidth + 'px';
    maxsize = 0;
    console.log('maxsize ' + maxsize);
  }

  if (n === 1) {
    expandedheight = expandedheight * 1.15;
    expandedwidth = expandedwidth * 1.15;
    expandedimg.style.width = expandedwidth + 'px';
    expandedimg.style.height = expandedheight + 'px';

    if (maxsize === 1) {
      expandedimg.style.width = expandedwidthbefore + 'px';
      expandedimg.style.height = expandedheightbefore + 'px';
    }
    if (expandedimg.width !== Math.round(expandedwidth) || expandedimg.height !== Math.round(expandedheight)) {
      maxsize = 1;
      console.log('maxsize ' + maxsize);
    }
  }
}

function save() {
  const saveButton = currentEntry.querySelector("button[aria-label='save']");
  const unsaveButton = currentEntry.querySelector("button[aria-label='unsave']");
  const moreButton = currentEntry.querySelector("button[aria-label='more']");
  if (saveButton) {
    saveButton.click();
  } else if (unsaveButton) {
    unsaveButton.click();
  } else {
    moreButton.click();
    if (saveButton) {
      saveButton.click();
    } else if (unsaveButton) {
      unsaveButton.click();
    }
  }
}

function edit() {
  let editButton = currentEntry.querySelector("button[aria-label='Edit']");
  let moreButton = currentEntry.querySelector("button[aria-label='more']");

  if (editButton) {
    editButton.click();
  } else {
    moreButton.click();
  }
}

function toggleExpand() {
  const expandButton = currentEntry.querySelector("button[aria-label='Expand here']");
  const textExpandButton = currentEntry.querySelector(".post-title>button");
  const commentExpandButton = currentEntry.querySelector(".ms-2>div>button");
  const moreExpandButton = currentEntry.querySelector(".ms-1>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(
          imgContainer,
          currentEntry.offsetHeight - imgContainer.offsetHeight + 10
        );
      }, true);
      currentEntry.getElementsByClassName("offset-sm-3 my-2 d-none d-sm-block")[0].className = "my-2 d-none d-sm-block";
    }
  }

  if (textExpandButton) {
    textExpandButton.click();

    const textContainers = [currentEntry.querySelector("#postContent"), currentEntry.querySelector(".card-body")];
    textContainers.forEach(container => {
      if (container) {
        scrollIntoViewWithOffset(
          container,
          currentEntry.offsetHeight - container.offsetHeight + 10
        );
      }
    });
  }

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

  if (moreExpandButton) {
    moreExpandButton.click();
    selectEntry(getPrevEntry(currentEntry), true);
  }
}

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
    });
  }

}

}