Listography Backup

Adds functionality for plaintext list export for backup purposes

当前为 2020-10-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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;
}

	`;
}