Skip to content

Instantly share code, notes, and snippets.

@slashedzer0
Created August 2, 2025 01:24
Show Gist options
  • Save slashedzer0/60b2cbbd9dc0e6786a5eb0dd2a6cf8a1 to your computer and use it in GitHub Desktop.
Save slashedzer0/60b2cbbd9dc0e6786a5eb0dd2a6cf8a1 to your computer and use it in GitHub Desktop.
adds feature in libreddit/redlib to save/unsave posts, which is saved in localStorage as post ids (won't work over incognito sessions)
// ==UserScript==
// @name redsave enhanced
// @namespace http://tampermonkey.net/
// @version 1.1
// @description adds feature in libreddit/redlib to save/unsave posts, which is saved in localStorage as post ids (won't work over incognito sessions)
// @author @yokelman, @slashedzer0
// @match https://redlib.perennialte.ch/*
// @icon https://redlib.perennialte.ch/favicon.ico
// @grant none
// @license GNU AGPL-3.0
// @downloadURL https://update.greasyfork.org/scripts/497793/redsave.user.js
// @updateURL https://update.greasyfork.org/scripts/497793/redsave.meta.js
// ==/UserScript==
(function() {
// !!!!!! THE SCRIPT CAN ACT WEIRDLY IF USING MULTIPLE TABS TO BROWSE THE SITE SINCE AFAIK LOCALSTORAGE ISN'T SYNCHRONIZED IN REAL TIME TO WORK ACROSS MULTIPLE TABS
// !!!!!! EXAMPLE: IF YOU SAVE POST A AND B ON THE MAIN PAGE, THEN OPEN POST A ON A NEW TAB, THEN UNSAVE POST B ON THE MAIN PAGE TAB AND THEN UNSAVE POST A THROUGH THE NEWLY OPENED TAB (WHICH ONLY CONTAINS A)...
// !!!!!! POST B (WHICH SHOULD HAVE BEEN UNSAVED THROUGH MAIN PAGE TAB) WILL STILL BE IN LOCALSTORAGE
// !!!!!! MORAL: DON'T GO SAVING/UNSAVING ON MULTIPLE PAGES AS FAR AS POSSIBLE, AND AFTER SAVING/UNSAVING ON ONE TAB IDEALLY RELOAD ALL THE OTHER TABS
// tested on v0.30.1, v0.31.0, v0.31.2, v0.34.0 on both libreddit and redlib which should be the majority of public instances
'use strict';
var domain = "https://redlib.perennialte.ch/";
// Flag to track if we're in saved posts view
var inSavedView = false;
// Check if the page loaded with the showSaved flag
const showSavedFlag = sessionStorage.getItem('showSaved');
if (showSavedFlag === 'true' && window.location.href === domain) {
// Remove the flag so it doesn't trigger on normal page loads
sessionStorage.removeItem('showSaved');
// Ensure the page is fully loaded before showing saved posts
if (document.readyState === 'complete') {
setTimeout(showSavedPosts, 300); // Small delay to ensure everything is ready
} else {
window.addEventListener('load', function() {
setTimeout(showSavedPosts, 300);
});
}
}
// load savedPosts from localStorage and handle if empty
var savedPosts = window.localStorage.getItem("savedPosts");
if (!savedPosts) {
savedPosts = [];
}
else {
savedPosts = savedPosts.split(",");
// if localStorage has savedPosts="", splitting gives you [""], don't want that empty string as an element
if (!savedPosts[0]) {
savedPosts.splice(0);
}
}
// adds an option to view saved posts when user clicks on Feeds at top left corner (if saved posts exist)
if (savedPosts.length) {
var link = document.createElement("a");
link.textContent = "Saved";
link.onclick = handleSavedClick;
link.style.cursor = "pointer"; // Fix #2: Set cursor to pointer
if (document.getElementById("feed_list").querySelectorAll("p").length == 2) {
document.getElementById("feed_list").insertBefore(link, document.getElementById("feed_list").children[4]);
}
else {
document.getElementById("feed_list").appendChild(link);
}
}
// Handle click on "Saved" link - redirect if needed or show saved posts directly
function handleSavedClick() {
if (window.location.href != domain) {
// Set a flag in sessionStorage to indicate we should show saved posts after redirect
sessionStorage.setItem('showSaved', 'true');
// Redirect to the home page
window.location.href = domain;
} else {
// Already on home page, show saved posts directly
showSavedPosts();
}
}
// manage saving/unsaving when save/unsave button is clicked
function manageSaved(id, removeElement = false) {
if (document.getElementsByClassName(id)[0].textContent == "save") {
savedPosts.push(id);
document.getElementsByClassName(id)[0].textContent = "unsave";
}
else {
savedPosts.splice(savedPosts.indexOf(id), 1);
document.getElementsByClassName(id)[0].textContent = "save";
// If we're in saved view and this is an unsave action, remove the post element
if (inSavedView && removeElement) {
const postElement = document.getElementById(id);
if (postElement) {
// If there's an HR element before this post, remove it too
const prevElement = postElement.previousElementSibling;
if (prevElement && prevElement.tagName === 'HR') {
prevElement.remove();
}
postElement.remove();
}
}
}
window.localStorage.setItem("savedPosts", savedPosts);
}
var post_footer = document.getElementsByClassName("post_footer");
// go post by post and add the save/unsave element
for (var i = 0; i < post_footer.length; i++) {
var save = document.createElement("a");
// the below is basically a somehow-works hack to get the post id (because the page could either have a collection of posts, or just a single post and i don't want to code for different cases) - can definitely lead to errors if site layout changes
var postId = post_footer[i].querySelector("a").href.split("/")[6];
if ((savedPosts.includes(postId))) {
save.textContent = "unsave";
}
else {
save.textContent = "save";
}
save.style = "font-weight: bold; cursor: pointer;"; // Fix #2: Added cursor: pointer
save.className = post_footer[i].querySelector("a").href.split("/")[6];
(function(localPostId) {save.onclick = function() {manageSaved(localPostId);};})(postId);
// if url includes comments, it means you're viewing an individual post (hopefully there's no loophole in this)
if (!window.location.href.includes("comments")) {
post_footer[i].appendChild(save);
}
else {
document.getElementById("post_links").appendChild(save);
}
}
// Original showSaved function renamed to showSavedPosts
async function showSavedPosts() {
if (!savedPosts.length) {
alert("No saved posts yet. Go ahead and save some!");
}
else {
// Set flag that we're in saved view
inSavedView = true;
// no pagination for now
if (document.querySelectorAll("footer").length == 2) {
document.querySelector("footer").remove();
}
if (document.querySelectorAll("form").length == 2) {
document.querySelectorAll("form")[1].remove();
}
document.title = "loading saved...";
var postPage;
var hr = document.createElement("hr");
hr.className = "sep";
document.getElementById("posts").innerHTML = "";
for (var j = savedPosts.length - 1; j > -1; j--) {
document.getElementById("posts").appendChild(hr.cloneNode());
await fetch(domain + "comments/" + savedPosts[j])
.then(response => {return response.text();})
.then(html => {postPage = new DOMParser().parseFromString(html, "text/html");
document.getElementById("posts").appendChild(postPage.getElementsByClassName("post")[0]);
console.log(j);
document.getElementsByClassName("post")[document.getElementsByClassName("post").length - 1].id = savedPosts[j];})
.catch(err => console.error("Error occurred: " + err));
}
for (var k = 0; k < savedPosts.length; k++) {
var save = document.createElement("a");
save.style = "font-weight: bold; cursor: pointer;"; // Fix #2: Added cursor: pointer
save.className = savedPosts[k];
save.textContent = "unsave";
// Add the ability to remove the post when unsaving in saved view
(function(index) {
save.onclick = function() {
manageSaved(savedPosts[index], true); // true means remove element when unsaving
};
})(k);
document.getElementById(savedPosts[k]).querySelector(".post_footer").children[0].appendChild(save);
}
document.title = "saved";
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment