// ==UserScript==
// @name Listography Backup
// @namespace petracoding
// @description Adds functionality for plaintext list export for backup purposes
// @version 0.0.2
// @author petracoding
// @match https://listography.com/*
// @match http://listography.com/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==
//////////////////////////////////// VARIABLES
const pathname = getPathOfUrl();
const userName = getPathOfUrl(
document.querySelector(".user-box .title a").href
).substring(1);
const userId = document
.querySelector(".about img")
.getAttribute("src")
.replace("/action/user-image?uid=", "");
const indexPath = "/" + userName + "/index";
let popup;
let popupoverlay;
let currentBatch = 1;
let listCount;
let listsToBackup = [];
let output = "";
//////////////////////////////////// START
$(document).ready(function () {
// only if the user is on their profile
if (document.querySelector(".global-menu .create-list")) {
HTML();
CSS();
// start backup if user clicked the backup all link and was redirected to the archive
if (pathname == indexPath + "?backup=true") {
startBackupAll();
}
console.log(":)");
}
});
//////////////////////////////////// BACKUP
async function startBackupAll() {
const listLinksToOpen = document.querySelectorAll(".body_folder .list a");
if (!listLinksToOpen) {
showPopup("No lists found.", true);
return;
}
startOutput();
await asyncForEach([...listLinksToOpen], async (link) => {
let list = await openListInArchive(link);
await editListAndAddToOutput(list);
});
finishOutput();
}
async function startBackupVisible() {
const listSelector = ".list-container";
const listSelectorInArchive = "#list_container .slot";
const listNodes = document.querySelectorAll(
listSelector + ", " + listSelectorInArchive
);
if (!listNodes || listNodes.length < 1) {
showPopup("No visible lists found.", true);
return;
}
listsToBackup = [...listNodes];
startOutput();
await asyncForEach(listsToBackup, async (list) => {
await editListAndAddToOutput(list);
});
finishOutput();
}
function openListInArchive(link) {
let listOpenPromise = new Promise(function (resolve, reject) {
link.click();
let listId = link.getAttribute("id").replace("list_" + userId + "_", "");
let attempt = 1;
let checkIfIsInEditMode = setInterval(function () {
if (document.querySelector("#listbox-" + listId + " .menu")) {
resolve(document.querySelector("#listbox-" + listId));
clearInterval(checkIfIsInEditMode);
} else {
if (attempt > 100) {
reject("List could not be backed up.");
clearInterval(checkIfIsInEditMode);
}
attempt = attempt + 1;
}
}, 100);
});
listOpenPromise.then(
function (list) {
return list;
},
function (errorMsg) {
alert(errorMsg);
}
);
return listOpenPromise;
}
function editListAndAddToOutput(list) {
let listEditPromise = new Promise(function (resolve, reject) {
const editButton = list.querySelector(".menu .item a[href*=edit-list]");
if (!editButton) {
reject("List could not be edited.");
}
editButton.click();
let attempt = 1;
let checkIfIsInEditMode = setInterval(function () {
if (list.querySelector(".category_editor")) {
let listContent = list.querySelector("textarea").innerHTML;
list.querySelector(".cancel.button_1_of_3").click();
resolve(listContent);
clearInterval(checkIfIsInEditMode);
} else {
if (attempt > 100) {
reject("List could not be backed up.");
clearInterval(checkIfIsInEditMode);
}
attempt = attempt + 1;
}
}, 100);
});
listEditPromise.then(
function (listContent) {
output +=
"\n\n\n--------------------------------------------------------\n\n\n";
output += getListOutput(list, listContent);
},
function (errorMsg) {
alert(errorMsg);
}
);
return listEditPromise;
}
//////////////////////////////////// HELPERS
function replaceAll(str, whatStr, withStr) {
return str.split(whatStr).join(withStr);
}
function getPathOfUrl(url, tld) {
let href;
if (!url) {
href = window.location.href;
} else {
href = url;
}
let ending;
if (!tld) {
ending = ".com/";
} else {
ending = "." + tld + "/";
}
return href.substring(href.indexOf(ending) + ending.length - 1);
}
function startOutput() {
document.querySelector("#backup-loading").style.display = "block";
popupoverlay.style.display = "block";
const url = location.href.replace("?backup=true", "");
output =
"<h1>Here are your lists:</h1><textarea id='backup-output'>Backup of " +
url;
}
function finishOutput() {
document.querySelector("#backup-loading").style.display = "none";
popupoverlay.style.display = "none";
showPopup(output + "</textarea>", true);
}
function getListOutput(list, listContent) {
if (!list || !listContent) return;
let listId;
if (list.querySelector(".listbox")) {
listId = list
.querySelector(".listbox")
.getAttribute("id")
.replace("listbox-", "");
} else {
listId = list
.querySelector("[id*=listbox-content-slot]")
.getAttribute("id")
.replace("listbox-content-slot-", "");
}
let listLink =
"Link: " + list.querySelector(".box-title a").getAttribute("href");
let listTitle = list
.querySelector(".box-title a")
.innerHTML.replace('<span class="box-subtitle">', "")
.replace("</span>", "")
.replace(/\s\s+/g, " ")
.trim();
let listDates =
"created on " +
list
.querySelector(".dates")
.innerHTML.replace("∞", "")
.replace("+", "")
.replace(" <br>", ", last updated on ")
.replace(/\s\s+/g, " ")
.trim();
let listImage = list.querySelector(".icon");
if (listImage) {
listImage =
"\nIcon: " + listImage.getAttribute("src").replace("&small=1", "");
} else {
listImage = "";
}
return (
listTitle +
"\n" +
listLink +
"\n(" +
listDates +
")" +
listImage +
"\n\n" +
adjustListContent(listContent, listId)
);
}
function adjustListContent(content, listId) {
// Add image urls
const attachmentUrl =
"https://listography.com/user/" +
userId +
"/list/" +
listId +
"/attachment/";
content = content.replace(/\[([a-z]+)\]/g, "[$1: " + attachmentUrl + "$1]");
return content;
}
// "forEach" is not async. here is our own async version of it.
// usage: await asyncForEach(myArray, async () => { ... })
const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
};
//////////////////////////////////// HTML
function HTML() {
createPopup();
createLoading();
createBackupAllButton();
createBackupVisibleButton();
}
function createBackupAllButton() {
const menu = document.querySelector(".global-menu tbody");
const tr = document.createElement("tr");
const td = document.createElement("td");
const button = document.createElement("input");
button.type = "button";
button.value = "backup all";
button.className = "backup-button";
if (onIndexPage()) {
button.onclick = startBackupAll;
} else {
button.onclick = goToIndex;
}
td.appendChild(button);
tr.appendChild(td);
menu.appendChild(tr);
}
function createBackupVisibleButton() {
const menu = document.querySelector(".global-menu tbody");
const tr = document.createElement("tr");
const td = document.createElement("td");
const button = document.createElement("input");
button.type = "button";
button.value = "backup visible";
button.className = "backup-button";
button.onclick = startBackupVisible;
td.appendChild(button);
tr.appendChild(td);
menu.appendChild(tr);
}
function onIndexPage() {
return pathname.startsWith(indexPath) && pathname.indexOf("?v") < 0;
}
function goToIndex() {
location.href = indexPath + "?backup=true";
}
function createPopup() {
popupoverlay = document.createElement("div");
popupoverlay.className = "backup-popup-overlay";
popup = document.createElement("div");
popup.className = "backup-popup";
document.body.appendChild(popup);
document.body.appendChild(popupoverlay);
hidePopup();
}
function createLoading() {
let loading = document.createElement("div");
loading.setAttribute("id", "backup-loading");
loading.style.display = "none";
loading.innerHTML =
"<h1>Loading...</h1><h2>Please wait.</h2>This may take a while if you have more than 100 lists.";
document.body.appendChild(loading);
}
function showPopup(text, allowClosing) {
if (text) popup.innerHTML = text;
popupoverlay.style.display = "block";
popup.style.display = "block";
document.body.style.overflow = "hidden";
if (allowClosing) {
popupoverlay.onclick = hidePopup;
}
}
function hidePopup() {
popup.style.display = "none";
popupoverlay.style.display = "none";
document.body.style.overflow = "auto";
}
//////////////////////////////////// CSS
function CSS() {
var styleSheet = document.createElement("style");
styleSheet.type = "text/css";
document.head.appendChild(styleSheet);
styleSheet.innerText = `
.backup-button {
background: none;
border: none;
color: rgb(119, 119, 119);
font-family: helvetica, arial, sans-serif;
font-size: 12px;
cursor: pointer;
font-style: italic;
}
.backup-button:hover, .backup-button:focus {
text-decoration: underline;
}
#backup-loading,
.backup-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 50px;
background: white;
z-index: 999;
border: 2px dotted lightgray;
border-radius: 5px;
min-width: 500px;
min-height: 200px;
justify-content: center;
align-items: center;
}
.backup-popup h1 {
margin-top: 0;
}
.backup-popup textarea {
width: 100%;
min-height: 300px;
}
.backup-popup-overlay {
content: "";
position: fixed;
z-index: 99;
background: rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
top: 0;
left: 0;
}
`;
}