Listography Backup

Adds functionality for plaintext list export for backup purposes

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

  1. // ==UserScript==
  2. // @name Listography Backup
  3. // @namespace petracoding
  4. // @description Adds functionality for plaintext list export for backup purposes
  5. // @version 0.0.2
  6. // @author petracoding
  7. // @match https://listography.com/*
  8. // @match http://listography.com/*
  9. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
  10. // ==/UserScript==
  11.  
  12. //////////////////////////////////// VARIABLES
  13.  
  14. const pathname = getPathOfUrl();
  15. const userName = getPathOfUrl(
  16. document.querySelector(".user-box .title a").href
  17. ).substring(1);
  18. const userId = document
  19. .querySelector(".about img")
  20. .getAttribute("src")
  21. .replace("/action/user-image?uid=", "");
  22. const indexPath = "/" + userName + "/index";
  23.  
  24. let popup;
  25. let popupoverlay;
  26. let currentBatch = 1;
  27. let listCount;
  28. let listsToBackup = [];
  29. let output = "";
  30.  
  31. //////////////////////////////////// START
  32.  
  33. $(document).ready(function () {
  34. // only if the user is on their profile
  35. if (document.querySelector(".global-menu .create-list")) {
  36. HTML();
  37. CSS();
  38.  
  39. // start backup if user clicked the backup all link and was redirected to the archive
  40. if (pathname == indexPath + "?backup=true") {
  41. startBackupAll();
  42. }
  43.  
  44. console.log(":)");
  45. }
  46. });
  47.  
  48. //////////////////////////////////// BACKUP
  49.  
  50. async function startBackupAll() {
  51. const listLinksToOpen = document.querySelectorAll(".body_folder .list a");
  52. if (!listLinksToOpen) {
  53. showPopup("No lists found.", true);
  54. return;
  55. }
  56.  
  57. startOutput();
  58.  
  59. await asyncForEach([...listLinksToOpen], async (link) => {
  60. let list = await openListInArchive(link);
  61. await editListAndAddToOutput(list);
  62. });
  63.  
  64. finishOutput();
  65. }
  66.  
  67. async function startBackupVisible() {
  68. const listSelector = ".list-container";
  69. const listSelectorInArchive = "#list_container .slot";
  70.  
  71. const listNodes = document.querySelectorAll(
  72. listSelector + ", " + listSelectorInArchive
  73. );
  74.  
  75. if (!listNodes || listNodes.length < 1) {
  76. showPopup("No visible lists found.", true);
  77. return;
  78. }
  79.  
  80. listsToBackup = [...listNodes];
  81.  
  82. startOutput();
  83.  
  84. await asyncForEach(listsToBackup, async (list) => {
  85. await editListAndAddToOutput(list);
  86. });
  87.  
  88. finishOutput();
  89. }
  90.  
  91. function openListInArchive(link) {
  92. let listOpenPromise = new Promise(function (resolve, reject) {
  93. link.click();
  94. let listId = link.getAttribute("id").replace("list_" + userId + "_", "");
  95.  
  96. let attempt = 1;
  97. let checkIfIsInEditMode = setInterval(function () {
  98. if (document.querySelector("#listbox-" + listId + " .menu")) {
  99. resolve(document.querySelector("#listbox-" + listId));
  100. clearInterval(checkIfIsInEditMode);
  101. } else {
  102. if (attempt > 100) {
  103. reject("List could not be backed up.");
  104. clearInterval(checkIfIsInEditMode);
  105. }
  106. attempt = attempt + 1;
  107. }
  108. }, 100);
  109. });
  110.  
  111. listOpenPromise.then(
  112. function (list) {
  113. return list;
  114. },
  115. function (errorMsg) {
  116. alert(errorMsg);
  117. }
  118. );
  119.  
  120. return listOpenPromise;
  121. }
  122.  
  123. function editListAndAddToOutput(list) {
  124. let listEditPromise = new Promise(function (resolve, reject) {
  125. const editButton = list.querySelector(".menu .item a[href*=edit-list]");
  126. if (!editButton) {
  127. reject("List could not be edited.");
  128. }
  129. editButton.click();
  130.  
  131. let attempt = 1;
  132. let checkIfIsInEditMode = setInterval(function () {
  133. if (list.querySelector(".category_editor")) {
  134. let listContent = list.querySelector("textarea").innerHTML;
  135. list.querySelector(".cancel.button_1_of_3").click();
  136. resolve(listContent);
  137. clearInterval(checkIfIsInEditMode);
  138. } else {
  139. if (attempt > 100) {
  140. reject("List could not be backed up.");
  141. clearInterval(checkIfIsInEditMode);
  142. }
  143. attempt = attempt + 1;
  144. }
  145. }, 100);
  146. });
  147.  
  148. listEditPromise.then(
  149. function (listContent) {
  150. output +=
  151. "\n\n\n--------------------------------------------------------\n\n\n";
  152.  
  153. output += getListOutput(list, listContent);
  154. },
  155. function (errorMsg) {
  156. alert(errorMsg);
  157. }
  158. );
  159.  
  160. return listEditPromise;
  161. }
  162.  
  163. //////////////////////////////////// HELPERS
  164.  
  165. function replaceAll(str, whatStr, withStr) {
  166. return str.split(whatStr).join(withStr);
  167. }
  168.  
  169. function getPathOfUrl(url, tld) {
  170. let href;
  171. if (!url) {
  172. href = window.location.href;
  173. } else {
  174. href = url;
  175. }
  176. let ending;
  177. if (!tld) {
  178. ending = ".com/";
  179. } else {
  180. ending = "." + tld + "/";
  181. }
  182. return href.substring(href.indexOf(ending) + ending.length - 1);
  183. }
  184.  
  185. function startOutput() {
  186. document.querySelector("#backup-loading").style.display = "block";
  187. popupoverlay.style.display = "block";
  188. const url = location.href.replace("?backup=true", "");
  189. output =
  190. "<h1>Here are your lists:</h1><textarea id='backup-output'>Backup of " +
  191. url;
  192. }
  193.  
  194. function finishOutput() {
  195. document.querySelector("#backup-loading").style.display = "none";
  196. popupoverlay.style.display = "none";
  197. showPopup(output + "</textarea>", true);
  198. }
  199.  
  200. function getListOutput(list, listContent) {
  201. if (!list || !listContent) return;
  202.  
  203. let listId;
  204. if (list.querySelector(".listbox")) {
  205. listId = list
  206. .querySelector(".listbox")
  207. .getAttribute("id")
  208. .replace("listbox-", "");
  209. } else {
  210. listId = list
  211. .querySelector("[id*=listbox-content-slot]")
  212. .getAttribute("id")
  213. .replace("listbox-content-slot-", "");
  214. }
  215.  
  216. let listLink =
  217. "Link: " + list.querySelector(".box-title a").getAttribute("href");
  218.  
  219. let listTitle = list
  220. .querySelector(".box-title a")
  221. .innerHTML.replace('<span class="box-subtitle">', "")
  222. .replace("</span>", "")
  223. .replace(/\s\s+/g, " ")
  224. .trim();
  225.  
  226. let listDates =
  227. "created on " +
  228. list
  229. .querySelector(".dates")
  230. .innerHTML.replace("∞", "")
  231. .replace("+", "")
  232. .replace(" <br>", ", last updated on ")
  233. .replace(/\s\s+/g, " ")
  234. .trim();
  235.  
  236. let listImage = list.querySelector(".icon");
  237. if (listImage) {
  238. listImage =
  239. "\nIcon: " + listImage.getAttribute("src").replace("&small=1", "");
  240. } else {
  241. listImage = "";
  242. }
  243.  
  244. return (
  245. listTitle +
  246. "\n" +
  247. listLink +
  248. "\n(" +
  249. listDates +
  250. ")" +
  251. listImage +
  252. "\n\n" +
  253. adjustListContent(listContent, listId)
  254. );
  255. }
  256.  
  257. function adjustListContent(content, listId) {
  258. // Add image urls
  259. const attachmentUrl =
  260. "https://listography.com/user/" +
  261. userId +
  262. "/list/" +
  263. listId +
  264. "/attachment/";
  265. content = content.replace(/\[([a-z]+)\]/g, "[$1: " + attachmentUrl + "$1]");
  266.  
  267. return content;
  268. }
  269.  
  270. // "forEach" is not async. here is our own async version of it.
  271. // usage: await asyncForEach(myArray, async () => { ... })
  272. const asyncForEach = async (array, callback) => {
  273. for (let index = 0; index < array.length; index++) {
  274. await callback(array[index], index, array);
  275. }
  276. };
  277.  
  278. //////////////////////////////////// HTML
  279.  
  280. function HTML() {
  281. createPopup();
  282. createLoading();
  283.  
  284. createBackupAllButton();
  285. createBackupVisibleButton();
  286. }
  287.  
  288. function createBackupAllButton() {
  289. const menu = document.querySelector(".global-menu tbody");
  290. const tr = document.createElement("tr");
  291. const td = document.createElement("td");
  292.  
  293. const button = document.createElement("input");
  294. button.type = "button";
  295. button.value = "backup all";
  296. button.className = "backup-button";
  297.  
  298. if (onIndexPage()) {
  299. button.onclick = startBackupAll;
  300. } else {
  301. button.onclick = goToIndex;
  302. }
  303.  
  304. td.appendChild(button);
  305. tr.appendChild(td);
  306. menu.appendChild(tr);
  307. }
  308.  
  309. function createBackupVisibleButton() {
  310. const menu = document.querySelector(".global-menu tbody");
  311. const tr = document.createElement("tr");
  312. const td = document.createElement("td");
  313.  
  314. const button = document.createElement("input");
  315. button.type = "button";
  316. button.value = "backup visible";
  317. button.className = "backup-button";
  318. button.onclick = startBackupVisible;
  319.  
  320. td.appendChild(button);
  321. tr.appendChild(td);
  322. menu.appendChild(tr);
  323. }
  324.  
  325. function onIndexPage() {
  326. return pathname.startsWith(indexPath) && pathname.indexOf("?v") < 0;
  327. }
  328.  
  329. function goToIndex() {
  330. location.href = indexPath + "?backup=true";
  331. }
  332.  
  333. function createPopup() {
  334. popupoverlay = document.createElement("div");
  335. popupoverlay.className = "backup-popup-overlay";
  336. popup = document.createElement("div");
  337. popup.className = "backup-popup";
  338.  
  339. document.body.appendChild(popup);
  340. document.body.appendChild(popupoverlay);
  341.  
  342. hidePopup();
  343. }
  344.  
  345. function createLoading() {
  346. let loading = document.createElement("div");
  347. loading.setAttribute("id", "backup-loading");
  348. loading.style.display = "none";
  349. loading.innerHTML =
  350. "<h1>Loading...</h1><h2>Please wait.</h2>This may take a while if you have more than 100 lists.";
  351.  
  352. document.body.appendChild(loading);
  353. }
  354.  
  355. function showPopup(text, allowClosing) {
  356. if (text) popup.innerHTML = text;
  357. popupoverlay.style.display = "block";
  358. popup.style.display = "block";
  359. document.body.style.overflow = "hidden";
  360.  
  361. if (allowClosing) {
  362. popupoverlay.onclick = hidePopup;
  363. }
  364. }
  365.  
  366. function hidePopup() {
  367. popup.style.display = "none";
  368. popupoverlay.style.display = "none";
  369. document.body.style.overflow = "auto";
  370. }
  371.  
  372. //////////////////////////////////// CSS
  373.  
  374. function CSS() {
  375. var styleSheet = document.createElement("style");
  376. styleSheet.type = "text/css";
  377. document.head.appendChild(styleSheet);
  378. styleSheet.innerText = `
  379.  
  380. .backup-button {
  381. background: none;
  382. border: none;
  383. color: rgb(119, 119, 119);
  384. font-family: helvetica, arial, sans-serif;
  385. font-size: 12px;
  386. cursor: pointer;
  387. font-style: italic;
  388. }
  389.  
  390. .backup-button:hover, .backup-button:focus {
  391. text-decoration: underline;
  392. }
  393.  
  394. #backup-loading,
  395. .backup-popup {
  396. position: fixed;
  397. top: 50%;
  398. left: 50%;
  399. transform: translate(-50%, -50%);
  400. padding: 50px;
  401. background: white;
  402. z-index: 999;
  403. border: 2px dotted lightgray;
  404. border-radius: 5px;
  405. min-width: 500px;
  406. min-height: 200px;
  407. justify-content: center;
  408. align-items: center;
  409. }
  410.  
  411. .backup-popup h1 {
  412. margin-top: 0;
  413. }
  414.  
  415. .backup-popup textarea {
  416. width: 100%;
  417. min-height: 300px;
  418. }
  419.  
  420. .backup-popup-overlay {
  421. content: "";
  422. position: fixed;
  423. z-index: 99;
  424. background: rgba(0, 0, 0, 0.5);
  425. width: 100%;
  426. height: 100%;
  427. top: 0;
  428. left: 0;
  429. }
  430.  
  431. `;
  432. }