Listography Backup

Adds functionality for plaintext list export for backup purposes

目前為 2020-10-26 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
}

	`;
}