JPDB-Export

Allows you to export your JPDB decks (see readme on github for more info)

目前为 2022-09-11 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name JPDB-Export
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.4
  5. // @description Allows you to export your JPDB decks (see readme on github for more info)
  6. // @author JaiWWW
  7. // @license GPL-3.0
  8. // @match https://jpdb.io/deck?*
  9. // @match https://jpdb.io/add-to-deck-from-shirabe-jisho*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=jpdb.io
  11. // @homepageURL https://github.com/JaiWWW/JPDB-Export
  12. // @supportURL https://github.com/JaiWWW/JPDB-Export/issues/new
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // ==/UserScript==
  16.  
  17. /*
  18.  
  19. Changelog:
  20. 1. Removed FileSaver.js from @require and pasted it below
  21. 2. Replaced progress test with working URL test to enable opening decks in another tab while an export is in progress
  22. > You should even be able to open the same deck in another tab but there is a very small chance to break it (avoidable)
  23. 3. Added a test to prevent multiple exports being attempted simultaneously (which would currently break the script)
  24. 4. Added debug and superdebug modes
  25. 5. Tweaked the match URL so the script doesn't run on the deck list page
  26. 6. Moved the working URL test above the page tweaks to save a bit of time and energy
  27.  
  28. */
  29.  
  30.  
  31.  
  32. // Start of required script: FileSaver.js by eligrey
  33. // https://github.com/eligrey/FileSaver.js
  34.  
  35. (function (global, factory) {
  36. if (typeof define === "function" && define.amd) {
  37. define([], factory);
  38. } else if (typeof exports !== "undefined") {
  39. factory();
  40. } else {
  41. var mod = {
  42. exports: {}
  43. };
  44. factory();
  45. global.FileSaver = mod.exports;
  46. }
  47. })(this, function () {
  48. "use strict";
  49.  
  50. /*
  51. * FileSaver.js
  52. * A saveAs() FileSaver implementation.
  53. *
  54. * By Eli Grey, http://eligrey.com
  55. *
  56. * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
  57. * source : http://purl.eligrey.com/github/FileSaver.js
  58. */
  59. // The one and only way of getting global scope in all environments
  60. // https://stackoverflow.com/q/3277182/1008999
  61. var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0;
  62.  
  63. function bom(blob, opts) {
  64. if (typeof opts === 'undefined') opts = {
  65. autoBom: false
  66. };else if (typeof opts !== 'object') {
  67. console.warn('Deprecated: Expected third argument to be a object');
  68. opts = {
  69. autoBom: !opts
  70. };
  71. } // prepend BOM for UTF-8 XML and text/* types (including HTML)
  72. // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
  73.  
  74. if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
  75. return new Blob([String.fromCharCode(0xFEFF), blob], {
  76. type: blob.type
  77. });
  78. }
  79.  
  80. return blob;
  81. }
  82.  
  83. function download(url, name, opts) {
  84. var xhr = new XMLHttpRequest();
  85. xhr.open('GET', url);
  86. xhr.responseType = 'blob';
  87.  
  88. xhr.onload = function () {
  89. saveAs(xhr.response, name, opts);
  90. };
  91.  
  92. xhr.onerror = function () {
  93. console.error('could not download file');
  94. };
  95.  
  96. xhr.send();
  97. }
  98.  
  99. function corsEnabled(url) {
  100. var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker
  101.  
  102. xhr.open('HEAD', url, false);
  103.  
  104. try {
  105. xhr.send();
  106. } catch (e) {}
  107.  
  108. return xhr.status >= 200 && xhr.status <= 299;
  109. } // `a.click()` doesn't work for all browsers (#465)
  110.  
  111.  
  112. function click(node) {
  113. try {
  114. node.dispatchEvent(new MouseEvent('click'));
  115. } catch (e) {
  116. var evt = document.createEvent('MouseEvents');
  117. evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
  118. node.dispatchEvent(evt);
  119. }
  120. } // Detect WebView inside a native macOS app by ruling out all browsers
  121. // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
  122. // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
  123.  
  124.  
  125. var isMacOSWebView = /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent);
  126. var saveAs = _global.saveAs || ( // probably in some web worker
  127. typeof window !== 'object' || window !== _global ? function saveAs() {}
  128. /* noop */
  129. // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
  130. : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) {
  131. var URL = _global.URL || _global.webkitURL;
  132. var a = document.createElement('a');
  133. name = name || blob.name || 'download';
  134. a.download = name;
  135. a.rel = 'noopener'; // tabnabbing
  136. // TODO: detect chrome extensions & packaged apps
  137. // a.target = '_blank'
  138.  
  139. if (typeof blob === 'string') {
  140. // Support regular links
  141. a.href = blob;
  142.  
  143. if (a.origin !== location.origin) {
  144. corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
  145. } else {
  146. click(a);
  147. }
  148. } else {
  149. // Support blobs
  150. a.href = URL.createObjectURL(blob);
  151. setTimeout(function () {
  152. URL.revokeObjectURL(a.href);
  153. }, 4E4); // 40s
  154.  
  155. setTimeout(function () {
  156. click(a);
  157. }, 0);
  158. }
  159. } // Use msSaveOrOpenBlob as a second approach
  160. : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
  161. name = name || blob.name || 'download';
  162.  
  163. if (typeof blob === 'string') {
  164. if (corsEnabled(blob)) {
  165. download(blob, name, opts);
  166. } else {
  167. var a = document.createElement('a');
  168. a.href = blob;
  169. a.target = '_blank';
  170. setTimeout(function () {
  171. click(a);
  172. });
  173. }
  174. } else {
  175. navigator.msSaveOrOpenBlob(bom(blob, opts), name);
  176. }
  177. } // Fallback to using FileReader and a popup
  178. : function saveAs(blob, name, opts, popup) {
  179. // Open a popup immediately do go around popup blocker
  180. // Mostly only available on user interaction and the fileReader is async so...
  181. popup = popup || open('', '_blank');
  182.  
  183. if (popup) {
  184. popup.document.title = popup.document.body.innerText = 'downloading...';
  185. }
  186.  
  187. if (typeof blob === 'string') return download(blob, name, opts);
  188. var force = blob.type === 'application/octet-stream';
  189.  
  190. var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
  191.  
  192. var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
  193.  
  194. if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') {
  195. // Safari doesn't allow downloading of blob URLs
  196. var reader = new FileReader();
  197.  
  198. reader.onloadend = function () {
  199. var url = reader.result;
  200. url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
  201. if (popup) popup.location.href = url;else location = url;
  202. popup = null; // reverse-tabnabbing #460
  203. };
  204.  
  205. reader.readAsDataURL(blob);
  206. } else {
  207. var URL = _global.URL || _global.webkitURL;
  208. var url = URL.createObjectURL(blob);
  209. if (popup) popup.location = url;else location.href = url;
  210. popup = null; // reverse-tabnabbing #460
  211.  
  212. setTimeout(function () {
  213. URL.revokeObjectURL(url);
  214. }, 4E4); // 40s
  215. }
  216. });
  217. _global.saveAs = saveAs.saveAs = saveAs;
  218.  
  219. if (typeof module !== 'undefined') {
  220. module.exports = saveAs;
  221. }
  222. });
  223.  
  224. // End of required script: FileSaver.js by eligrey
  225. // https://github.com/eligrey/FileSaver.js
  226.  
  227.  
  228.  
  229. (function() {
  230. 'use strict';
  231.  
  232. let debug = false; // Set true to enter debug mode
  233. // Any line beginning with "debug &&" will only run if this is set to true
  234. let superdebug = false; // Creates way more console logs
  235. // Any line beginning with "superdebug &&" will only run if this is set to true
  236.  
  237. if (!GM_getValue('debugPass') && (debug || superdebug)) { // If debug and/or super debug mode is enabled AND debugPass is falsy
  238. if (window.confirm("Looks like you have debug mode enabled. Are you sure you want to continue?")) {
  239. if (superdebug) { // If super debug mode is enabled
  240. if (window.confirm("Are you really sure you want to continue in super debug mode? This will drown your console!")) {
  241. debug = true; // Just in case you only turned on superdebug - how naughty!
  242. console.log("JPDB-Export successfully launched with super debug mode enabled.");
  243. } else {
  244. superdebug = false;
  245. console.log("JPDB-Export successfully launched with debug mode enabled.");
  246. }
  247. } else {
  248. console.log("JPDB-Export successfully launched with debug mode enabled.");
  249. }
  250. } else {
  251. debug = false;
  252. superdebug = false;
  253. }
  254. }
  255.  
  256. const URL = window.location.href;
  257. debug && console.log(`URL = ${URL}`);
  258.  
  259. if (URL.startsWith('https://jpdb.io/deck')) { // Deck page
  260.  
  261. debug && console.log("Deck page detected");
  262.  
  263. let workingURL = GM_getValue('workingURL'); // if export is in progress, workingURL will store the URL of the next page to be exported
  264. debug && console.log(`workingURL = ${workingURL}`);
  265.  
  266. function exportToCSV() { // Export the deck contents to a CSV file
  267.  
  268. debug && console.log("Called exportToCSV()");
  269. debug && GM_setValue('debugPass', true) // Skip debug check until export is finished
  270.  
  271. // Test if FileSaver.js is supported
  272. let supported;
  273. try {
  274. const isFileSaverSupported = !!new Blob;
  275. // isFileSaverSupported = 1; // Uncomment to test what happens on unsupported browsers
  276. debug && console.log("Download script supported");
  277. supported = true;
  278. } catch (e) {
  279. debug && console.log("Download script not supported");
  280. supported = false;
  281. const errorMessage = 'Userscript "JPDB-Export":\n\nSorry, your browser does not support the system used to download files. Please see this link for more information: https://github.com/eligrey/FileSaver.js#user-content-supported-browsers\n\nDo you want to go to this link now? (opens in new tab)';
  282. if (confirm(errorMessage)) { // If they click OK to go to the link
  283. window.open("https://github.com/eligrey/FileSaver.js#user-content-supported-browsers");
  284. }
  285. }
  286.  
  287. if (supported) {
  288.  
  289. debug && console.log("Script passed supported check, now running simultaneous export check");
  290.  
  291. workingURL = GM_getValue('workingURL'); // Refreshing workingURL so that the alreadyExporting check works without a refresh
  292. debug && console.log(`workingURL = ${workingURL}`);
  293. const alreadyExporting = "Sorry, it seems like you already have an export in progress somewhere else. This script does not currently support simultaneous exports.\n\nIf this is an error, please report it on github by pressing the bug icon in your userscript manager's panel or dashboard. Thanks!";
  294. if (workingURL && URL != workingURL) { // workingURL active and on a different URL - i.e. an export is in progress somewhere else
  295. debug && console.log("Simultaneous export detected");
  296. return window.alert(alreadyExporting);
  297. }
  298.  
  299. const confirmationMessage = "Exporting your deck may take some time if it has a lot of pages. Continue?";
  300. if ((URL === workingURL) || confirm(confirmationMessage)) { // If they are already in progress or click OK to start export
  301.  
  302. debug && console.log("Export confirmed. Attempting to start export");
  303.  
  304. function addPageToFile() { // Append the current page to the file
  305. debug && console.log("Called addPageToFile()");
  306.  
  307. const vocabList = document.querySelector("body > div.container.bugfix > div.vocabulary-list"); // Div containing all the vocab on the page
  308. debug && console.log("vocabList:", vocabList);
  309.  
  310. let wordWrapper; // The <a> tag surrounding each word
  311. let stringK; // This will store the current kanji string being found to add to the CSV
  312. let stringR; // This will store the current reading string being found to add to the CSV
  313. let fileContents = GM_getValue('fileContents'); // Get the current file contents into fileContents
  314. debug && console.log("File is currently", fileContents.split("\n").length-1, "lines long.");
  315.  
  316. debug && console.log("Looping through each element in the vocab list:");
  317. for (let i = 1; i <= vocabList.childElementCount; i++) { // Loop through each word in the vocab list
  318.  
  319. superdebug && console.log(`Word number ${i}`);
  320. stringK = '';
  321. stringR = '';
  322. wordWrapper = vocabList.querySelector(`div:nth-child(${i}) > div:nth-child(1) > div.vocabulary-spelling > a`);
  323. superdebug && console.log("wordWrapper:", wordWrapper);
  324. for (let j = 0; j < wordWrapper.childElementCount; j++) { // Loop through each ruby element in this word
  325. superdebug && console.log("Checking:", wordWrapper.children[j]);
  326. if (wordWrapper.children[j].childElementCount === 0) { // If this ruby element has no children (i.e. it's kana)
  327. superdebug && console.log("It's kana, adding to both strings");
  328. // Add the contents to both strings
  329. stringR += wordWrapper.children[j].textContent;
  330. stringK += wordWrapper.children[j].textContent;
  331. } else { // If this ruby element is kanji
  332. superdebug && console.log("It's kanji, adding kanji to stringK and furigana to stringR");
  333. stringK += wordWrapper.children[j].firstChild.textContent; // Add the kanji to the kanji string
  334. stringR += wordWrapper.children[j].children[0].textContent; // Add the furigana to the reading string
  335. }
  336. }
  337. superdebug && console.log(`Adding the following line to fileContents: "${stringK},${stringR}," plus a line break`);
  338. fileContents += `${stringK},${stringR},\n`; // Append the correctly formatted strings to fileContents
  339. }
  340. debug && console.log("Adding page to 'fileContents'");
  341. GM_setValue('fileContents', fileContents); // Update the file contents
  342. }
  343.  
  344. function createFileName() { // Create a file name and upload to storage
  345. debug && console.log("Called createFileName()");
  346.  
  347. const container = document.querySelector("body > div.container.bugfix");
  348. debug && console.log("container:", container);
  349. const deckName = container.firstChild.nextSibling.textContent;
  350. superdebug && console.log(`deckName = ${deckName}`);
  351. const current = new Date();
  352. const time = current.toLocaleTimeString();
  353. const date = current.toLocaleDateString();
  354. const fileName = `_${deckName}_ deck export at ${time} on ${date}.csv`;
  355. debug && console.log(`filename = ${fileName}, returning fileName`);
  356. return fileName;
  357. }
  358.  
  359. function downloadFile() { // Download the file
  360. debug && console.log("Called downloadFile()");
  361.  
  362. superdebug && console.log("Attempting call createFileName()");
  363. const fileName = createFileName();
  364. superdebug && console.log("Getting file contents");
  365. const fileContents = GM_getValue('fileContents');
  366.  
  367. const blob = new Blob([fileContents], {type: "text/plain;charset=utf-8"});
  368. debug && console.log("Saving file");
  369. saveAs(blob, fileName);
  370. GM_setValue('workingURL', ''); // Clear working URL
  371. GM_setValue('debugPass', false) // Clear debug pass
  372. }
  373.  
  374. function lastPage() { // Test if we are on the last page or not
  375. debug && console.log("Called lastPage()");
  376.  
  377. const pagination = document.querySelector("body > div.container.bugfix > div.pagination"); // Div that shows "Next page"
  378. debug && console.log("pagination:", pagination);
  379. if (pagination.textContent.indexOf("Next page") < 0) { // Last page
  380. debug && console.log("Last page detected");
  381. return true;
  382. } else { // More pages to go
  383. debug && console.log("More pages detected");
  384. return false;
  385. }
  386. }
  387.  
  388. if (URL.indexOf("offset=") < 0) { // If they are on the first page of the deck
  389.  
  390. debug && console.log("First page detected.");
  391.  
  392. GM_setValue('fileContents', ''); // Initiate fileContents
  393. superdebug && console.log("fileContents initiated.");
  394. superdebug && console.log("Attempting to call addPageToFile()");
  395. addPageToFile();
  396.  
  397. superdebug && console.log("Attempting to call lastPage()");
  398. if (lastPage()) { // Download file
  399. superdebug && console.log("Attempting to call downloadFile()");
  400. downloadFile();
  401. } else { // Redirect to next page
  402.  
  403. // URL looks like 'https://jpdb.io/deck?id=123' potentially with '#a' at the end
  404. const redirect = URL.replace('#a', '') + '&offset=50';
  405. debug && console.log(`Redirecting to ${redirect}`);
  406. GM_setValue('workingURL', redirect);
  407. window.location.replace(redirect);
  408. }
  409.  
  410. } else { // They are not on the first page
  411. debug && console.log("Non-first page detected");
  412.  
  413. if (URL === workingURL) { // If export is already in progress and they are on the right page
  414.  
  415. superdebug && console.log("Attempting to call addPage()");
  416. addPageToFile();
  417.  
  418. superdebug && console.log("Attempting to call lastPage()");
  419. if (lastPage()) { // Download file
  420. superdebug && console.log("Attempting to call downloadFile()");
  421. downloadFile();
  422. } else { // Redirect to next page
  423.  
  424. // URL looks like 'https://jpdb.io/deck=123&offset=200'
  425. const offsetIndex = URL.indexOf("offset=") + 7; // First character of the actual offset number
  426. const offset = parseInt(URL.slice(offsetIndex)) + 50; // Offset value of next page
  427. superdebug && console.log(`Next offset = ${offset}`);
  428.  
  429. const redirect = URL.slice(0, offsetIndex) + offset;
  430. debug && console.log(`Redirecting to ${redirect}`);
  431. GM_setValue('workingURL', redirect)
  432. window.location.replace(redirect);
  433. }
  434.  
  435.  
  436. } else { // User wants to start export but has to go to the first page
  437.  
  438. const firstPage = URL.slice(0,URL.indexOf("offset=")-1);
  439.  
  440. debug && console.log(`Redirecting to ${firstPage}`);
  441. GM_setValue('workingURL', firstPage);
  442. window.location.replace(firstPage); // Go to first page
  443.  
  444. }
  445. }
  446. }
  447. }
  448. }
  449.  
  450. if (URL === workingURL) {
  451. debug && console.log("URL matches working URL");
  452. exportToCSV();
  453. } else { // Don't bother tweaking the page if we're alredy in progress
  454. const menu = document.querySelector("body > div.container.bugfix > div.dropdown > details > div").firstChild; // UL of options in the menu
  455. debug && console.log("menu:", menu);
  456. const shirabe = menu.getElementsByTagName("li")[5]; // The "Import from Shirabe Jisho" button
  457. debug && console.log("shirabe:", shirabe);
  458. shirabe.firstChild.lastChild.setAttribute('value', 'Import from CSV'); // Rename to "Import from CSV"
  459. debug && console.log("Renamed import button");
  460.  
  461. // Add "Export to CSV" button
  462. shirabe.insertAdjacentHTML('afterend', '<li id="export"><form class="link-like" method="dialog"><input type="submit" value="Export to CSV"></form></li>');
  463. debug && console.log("Added export button");
  464.  
  465. const exportButton = document.getElementById("export");
  466. exportButton.addEventListener('click', exportToCSV); // Call exportToCSV() when exportButton is clicked
  467. debug && console.log("Added event listener to export button");
  468. }
  469.  
  470.  
  471.  
  472.  
  473. }
  474.  
  475. if (URL.startsWith('https://jpdb.io/add-to')) { // Import page
  476.  
  477. debug && console.log("Import page detected");
  478.  
  479. const heading = document.querySelector("body > div.container.bugfix > h4") // "Import from Shirabe Jisho" heading
  480. debug && console.log("heading:", heading);
  481. heading.innerHTML = 'Import from CSV'; // Changing the heading
  482. debug && console.log("Changed heading");
  483.  
  484. const bulletOne = document.querySelector("body > div.container.bugfix > ul > li:nth-child(1)"); // First bullet point
  485. debug && console.log("bulletOne:", bulletOne);
  486. bulletOne.innerHTML += ', decks or any other correctly-formatted CSV file';
  487. debug && console.log("Edited first bullet point");
  488. // Add a bullet point explaining how to find the correct format
  489. bulletOne.insertAdjacentHTML(
  490. 'afterend', '<li>To see an example of the correct format, try exporting a deck and opening the file in a text editor</li>');
  491. debug && console.log("Inserted a new bullet point");
  492. }
  493. })();