[GC] - SDB Contents Collection

2/25/2024, 11:26:31 PM

  1. // ==UserScript==
  2. // @name [GC] - SDB Contents Collection
  3. // @namespace https://greasyfork.org/en/users/1225524-kaitlin
  4. // @match https://www.grundos.cafe/safetydeposit/*
  5. // @grant GM.setValue
  6. // @grant GM.getValue
  7. // @grant GM.addStyle
  8.  
  9. // @license MIT
  10. // @version 1.4
  11. // @author Cupkait
  12. // @icon https://i.imgur.com/4Hm2e6z.png
  13. // @description 2/25/2024, 11:26:31 PM
  14. // ==/UserScript==
  15.  
  16. // State variables
  17. let collect = false;
  18. let active = false;
  19. let customSearchUrl = null;
  20.  
  21. // Constants
  22. const MAIN_SELECTOR = 'main .sdb-info';
  23.  
  24. // Initialize style and UI
  25. const sdbStyle = addStyles();
  26. const collectContainer = createCollectContainer();
  27. document.querySelector(MAIN_SELECTOR).append(collectContainer);
  28.  
  29. // Start script
  30. initialize();
  31.  
  32. async function initialize() {
  33. collect = await GM.getValue('collect', false);
  34. active = await GM.getValue('active', false);
  35. customSearchUrl = await GM.getValue('customSearchUrl', null);
  36.  
  37. if (collect) {
  38. showNextButtons();
  39. } else if (active) {
  40. enableNextPageNavigation();
  41. } else {
  42. showEnableButton();
  43. }
  44. }
  45.  
  46. function addStyles() {
  47. const style = document.createElement('style');
  48. style.innerHTML = `
  49. .collect-sdb {
  50. text-align: center;
  51. margin: 10px;
  52. }
  53. .collect-sdb a {
  54. background-color: var(--grid_head);
  55. border: 1px solid grey;
  56. padding: 5px 15px;
  57. margin: 10px;
  58. cursor: pointer;
  59. display: inline-block;
  60. }
  61. .completion-message {
  62. color: green;
  63. font-weight: bold;
  64. }
  65. .add-custom-search {
  66. display: inline-flex;
  67. align-items: center;
  68. justify-content: center;
  69. width: 30px;
  70. height: 30px;
  71. border-radius: 50%;
  72. background-color: var(--grid_head);
  73. border: 1px solid grey;
  74. cursor: pointer;
  75. font-size: 20px;
  76. vertical-align: middle;
  77. margin: 10px;
  78. }
  79. .custom-search-container {
  80. margin: 10px 0;
  81. display: flex;
  82. justify-content: center;
  83. align-items: center;
  84. flex-wrap: wrap;
  85. }
  86. .custom-search-input {
  87. padding: 5px;
  88. margin-right: 10px;
  89. width: 60%;
  90. border: 1px solid grey;
  91. }
  92. .custom-search-button {
  93. background-color: var(--grid_head);
  94. border: 1px solid grey;
  95. padding: 5px 15px;
  96. cursor: pointer;
  97. }
  98. `;
  99. document.head.appendChild(style);
  100. return style;
  101. }
  102.  
  103. function createCollectContainer() {
  104. const container = document.createElement('div');
  105. container.classList.add('collect-sdb');
  106. return container;
  107. }
  108.  
  109. function createButton(text, onClick) {
  110. const button = document.createElement('a');
  111. button.textContent = text;
  112. button.addEventListener('click', onClick);
  113. return button;
  114. }
  115.  
  116. function removeAllButtons() {
  117. collectContainer.innerHTML = '';
  118. }
  119.  
  120. function showEnableButton() {
  121. collectContainer.append(createButton('Begin SDB Data Collection', async () => {
  122. await GM.setValue('collect', true);
  123. showNextButtons();
  124. }));
  125. }
  126.  
  127. function showNextButtons() {
  128. removeAllButtons();
  129.  
  130. // Add standard collection buttons
  131. collectContainer.append(
  132. createButton('Collect full SDB', () => setActiveAndNavigate('https://www.grundos.cafe/safetydeposit/')),
  133. createButton('Collect quest only', () => setActiveAndNavigate('https://www.grundos.cafe/safetydeposit/?page=1&&max_rarity=89&view=100'))
  134. );
  135.  
  136. // Add custom search button if a URL exists
  137. if (customSearchUrl) {
  138. const customButton = createButton('Custom Search', () => setActiveAndNavigate(customSearchUrl));
  139. const resetButton = createButton('Reset Custom', resetCustomSearch);
  140. collectContainer.append(customButton, resetButton);
  141. } else {
  142. // Add the plus button to add a custom search
  143. const addButton = document.createElement('span');
  144. addButton.innerHTML = '+'; // Plus sign
  145. addButton.title = 'Add custom search URL';
  146. addButton.classList.add('add-custom-search');
  147. addButton.addEventListener('click', showCustomSearchInput);
  148. collectContainer.append(addButton);
  149. }
  150. }
  151.  
  152. function showCustomSearchInput() {
  153. // Remove the plus button
  154. const plusButton = document.querySelector('.add-custom-search');
  155. if (plusButton) plusButton.remove();
  156.  
  157. // Create container for input and save button
  158. const customContainer = document.createElement('div');
  159. customContainer.classList.add('custom-search-container');
  160.  
  161. // Create input for URL
  162. const urlInput = document.createElement('input');
  163. urlInput.type = 'text';
  164. urlInput.placeholder = 'Paste your custom scan URL here and save';
  165. urlInput.classList.add('custom-search-input');
  166.  
  167. // Create save button
  168. const saveButton = document.createElement('button');
  169. saveButton.textContent = 'Save';
  170. saveButton.classList.add('custom-search-button');
  171. saveButton.addEventListener('click', () => saveCustomSearch(urlInput.value));
  172.  
  173. // Create cancel button
  174. const cancelButton = document.createElement('button');
  175. cancelButton.textContent = 'Cancel';
  176. cancelButton.classList.add('custom-search-button');
  177. cancelButton.style.marginLeft = '5px';
  178. cancelButton.addEventListener('click', () => {
  179. customContainer.remove();
  180. showNextButtons(); // Re-show the buttons including the plus sign
  181. });
  182.  
  183. // Append elements
  184. customContainer.append(urlInput, saveButton, cancelButton);
  185. collectContainer.append(customContainer);
  186.  
  187. // Focus the input
  188. urlInput.focus();
  189. }
  190.  
  191. async function saveCustomSearch(url) {
  192. if (!url || !url.trim()) {
  193. displayMessage('Please enter a valid URL');
  194. return;
  195. }
  196.  
  197. // Validate URL format
  198. if (!url.startsWith('https://www.grundos.cafe/safetydeposit/')) {
  199. displayMessage('URL must start with https://www.grundos.cafe/safetydeposit/');
  200. return;
  201. }
  202.  
  203. // Save the URL
  204. customSearchUrl = url.trim();
  205. await GM.setValue('customSearchUrl', customSearchUrl);
  206.  
  207. // Remove the input container
  208. const customContainer = document.querySelector('.custom-search-container');
  209. if (customContainer) customContainer.remove();
  210.  
  211. // Show success message
  212. displayMessage('Custom search URL saved!');
  213.  
  214. // Redisplay buttons
  215. showNextButtons();
  216. }
  217.  
  218. async function resetCustomSearch() {
  219. customSearchUrl = null;
  220. await GM.setValue('customSearchUrl', null);
  221. showNextButtons();
  222. displayMessage('Custom search has been reset.');
  223. }
  224.  
  225. async function setActiveAndNavigate(url) {
  226. await Promise.all([
  227. GM.setValue('collect', false),
  228. GM.setValue('active', true)
  229. ]);
  230. window.location.href = url;
  231. }
  232.  
  233. function enableNextPageNavigation() {
  234. removeAllButtons();
  235. collectContainer.append(createButton('Cancel/Restart Collection', cancelCollection));
  236. initTableProcessing();
  237. }
  238.  
  239. function appendMessage(messageText) {
  240. const message = document.createElement('p');
  241. message.innerHTML = messageText;
  242. collectContainer.append(message);
  243. }
  244.  
  245. async function initTableProcessing() {
  246. await processTableData();
  247. displayPageInfo();
  248. setupKeyboardNavigation();
  249. }
  250.  
  251. async function loadSdbContents() {
  252. return await GM.getValue('sdbContents', []);
  253. }
  254.  
  255. async function saveSdbContents(contents) {
  256. await GM.setValue('sdbContents', contents);
  257. }
  258.  
  259. async function processTableData() {
  260. const sdbContents = await loadSdbContents();
  261. const data = document.querySelectorAll('.data');
  262. const rows = [];
  263.  
  264. for (let i = 0; i < data.length; i += 7) {
  265. if (!data[i + 1] || !data[i + 2] || !data[i + 3] || !data[i + 4]) continue;
  266.  
  267. const row = createRow(data, i);
  268. if (!row) continue;
  269.  
  270. const existingItemIndex = sdbContents.findIndex(item => item.n === row.n);
  271. if (existingItemIndex > -1) {
  272. sdbContents[existingItemIndex] = row;
  273. } else {
  274. sdbContents.push(row);
  275. }
  276. rows.push(row);
  277. }
  278.  
  279. await saveSdbContents(sdbContents);
  280. }
  281.  
  282. function createRow(data, index) {
  283. try {
  284. const nameElement = data[index + 1].querySelector('strong');
  285. const rarityElement = data[index + 1].querySelector('span');
  286. const imageElement = data[index + 2].querySelector('img');
  287. const quantityElement = data[index + 3];
  288. const typeElement = data[index + 4].querySelector('strong');
  289.  
  290. if (!nameElement || !rarityElement || !imageElement || !quantityElement || !typeElement) {
  291. return null;
  292. }
  293.  
  294. const rarityMatch = rarityElement.textContent.match(/\d+/);
  295. if (!rarityMatch) return null;
  296.  
  297. return {
  298. n: nameElement.textContent,
  299. r: parseInt(rarityMatch[0]),
  300. p: imageElement.src.split('/').pop(),
  301. q: parseInt(quantityElement.textContent) || 0,
  302. t: typeElement.textContent,
  303. };
  304. } catch (error) {
  305. console.error('Error creating row:', error);
  306. return null;
  307. }
  308. }
  309.  
  310. function setupKeyboardNavigation() {
  311. document.addEventListener('keydown', (event) => {
  312. if (event.key === 'ArrowRight' && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
  313. const nextPageLink = [...document.querySelectorAll('.center a')].find(link =>
  314. link.textContent.trim().startsWith('Next'));
  315. if (nextPageLink) nextPageLink.click();
  316. }
  317. });
  318. }
  319.  
  320. async function displayPageInfo() {
  321. const pageOptions = document.querySelectorAll('#page option');
  322. const pageCount = pageOptions.length;
  323. const currentPage = parseInt(new URLSearchParams(window.location.search).get('page')) || 1;
  324. const sdbContents = await loadSdbContents();
  325. const totalItems = sdbContents.length;
  326.  
  327. // Get end total (if available)
  328. let endTotal = 0;
  329. const totalText = document.querySelector('main div:nth-child(6)')?.childNodes[4]?.textContent;
  330. if (totalText) {
  331. endTotal = parseFloat(totalText.replace(/[^0-9.]/g, '').replace(/,/g, ''));
  332. }
  333.  
  334. appendMessage(`Total items collected: <strong>${totalItems.toLocaleString()}</strong> <br>You are viewing page <strong>${currentPage.toLocaleString()}</strong> / <strong>${pageCount.toLocaleString()}</strong>.`);
  335. appendMessage(`Click "Next" or press the right arrow key to go to the next page.`);
  336.  
  337. // Always show export buttons
  338. appendExportButtons();
  339. }
  340.  
  341. function appendExportButtons() {
  342. // Check if buttons already exist
  343. if (!document.querySelector('.collect-sdb .export-csv-button') &&
  344. !document.querySelector('.collect-sdb .copy-clipboard-button')) {
  345.  
  346. const csvButton = createButton('Export to CSV', exportToCSV);
  347. csvButton.classList.add('export-csv-button');
  348.  
  349. const clipboardButton = createButton('Copy to Clipboard', copyToClipboard);
  350. clipboardButton.classList.add('copy-clipboard-button');
  351.  
  352. collectContainer.append(csvButton, clipboardButton);
  353. appendMessage(`Export to CSV to make a spreadsheet of the results. Copy to Clipboard to paste into a Virtupets.net Checklist.`);
  354. }
  355. }
  356.  
  357. async function exportToCSV() {
  358. try {
  359. const sdbContents = await loadSdbContents();
  360. if (sdbContents.length === 0) {
  361. displayMessage('No data to export. Please collect some data first.');
  362. return;
  363. }
  364.  
  365. const csvContent = "data:text/csv;charset=utf-8," +
  366. "Name,Rarity,Image,Quantity,Type\n" +
  367. sdbContents.map(e => Object.values(e).join(",")).join("\n");
  368.  
  369. const link = document.createElement("a");
  370. link.setAttribute("href", encodeURI(csvContent));
  371. link.setAttribute("download", `sdbContents_${new Date().toISOString().slice(0,10)}.csv`);
  372. document.body.appendChild(link);
  373. link.click();
  374. link.remove();
  375.  
  376. displayMessage('Export complete!');
  377. } catch (err) {
  378. console.error('Error exporting to CSV:', err);
  379. displayMessage('Error exporting to CSV. Check console for details.');
  380. }
  381. }
  382.  
  383. async function copyToClipboard() {
  384. try {
  385. const sdbContents = await loadSdbContents();
  386. if (sdbContents.length === 0) {
  387. displayMessage('No data to copy. Please collect some data first.');
  388. return;
  389. }
  390.  
  391. await navigator.clipboard.writeText(JSON.stringify(sdbContents));
  392. displayMessage('Copy complete!');
  393. } catch (err) {
  394. console.error('Error copying to clipboard:', err);
  395. displayMessage('Error copying to clipboard. Check console for details.');
  396. }
  397. }
  398.  
  399. function displayMessage(text) {
  400. let message = collectContainer.querySelector('.completion-message');
  401.  
  402. if (!message) {
  403. message = document.createElement('p');
  404. message.classList.add('completion-message');
  405. collectContainer.append(message);
  406. }
  407. message.innerHTML = text;
  408.  
  409. // Auto-clear message after 5 seconds
  410. setTimeout(() => {
  411. if (message.parentNode) {
  412. message.innerHTML = '';
  413. }
  414. }, 5000);
  415. }
  416.  
  417. async function cancelCollection() {
  418. await Promise.all([
  419. GM.setValue('active', false),
  420. GM.setValue('sdbContents', [])
  421. ]);
  422. window.location.href = 'https://www.grundos.cafe/safetydeposit/';
  423. }