Add non-MFC figure

Track preordered non-MFC items on collection screen

  1. // ==UserScript==
  2. // @name Add non-MFC figure
  3. // @namespace https://tharglet.me.uk
  4. // @version 2.6
  5. // @description Track preordered non-MFC items on collection screen
  6. // @author Tharglet
  7. // @match https://myfigurecollection.net/?*mode=view&*tab=collection&*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=myfigurecollection.net
  9. // @grant GM_addStyle
  10. // @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. //Polyfill for GM_addStyle for Greasemonkey...
  16. if(typeof GM_addStyle == 'undefined') {
  17. GM_addStyle = (aCss) => {
  18. 'use strict';
  19. let head = document.getElementsByTagName('head')[0];
  20. if (head) {
  21. let style = document.createElement('style');
  22. style.setAttribute('type', 'text/css');
  23. style.textContent = aCss;
  24. head.appendChild(style);
  25. return style;
  26. }
  27. return null;
  28. };
  29. }
  30.  
  31. GM_addStyle(`
  32. .non-mfc-item__tray-icon {
  33. border: green 1px solid;
  34. display: block;
  35. margin: 8px;
  36. padding: 1px;
  37. }
  38. .non-mfc-item__icon-container {
  39. float: left;
  40. margin-bottom: 20px;
  41. }
  42. .non-mfc-item__tray-icon img {
  43. width: 60px;
  44. height: 60px;
  45. border-radius: 2px;
  46. }
  47. .non-mfc-item__edit, .non-mfc-item__delete {
  48. display: inline-block;
  49. }
  50.  
  51. .non-mfc-item__edit span, .non-mfc-item__delete span {
  52. font-size: 16px;
  53. margin: 0 4px;
  54. }
  55.  
  56. /* Pip styles */
  57. .item-custompip {
  58. display: block;
  59. position: absolute;
  60. right: 1px;
  61. bottom: 1px;
  62. height: 16px;
  63. padding: 1px 2px 2px 3px;
  64. line-height: 16px;
  65. color: white;
  66. }
  67.  
  68. .item-is-paid {
  69. background-color: green;
  70. }
  71.  
  72. .item-is-shipped {
  73. background-color: gold;
  74. }
  75.  
  76. .item-is-stored {
  77. background-color: orangered;
  78. }
  79.  
  80. .icon-dollar:before {
  81. font-family: serif !important;
  82. content: "$";
  83. font-weight: bolder !important;
  84. }
  85.  
  86. .icon-plane:before {
  87. font-family: serif !important;
  88. content: "🛩️";
  89. font-weight: bolder !important;
  90. }
  91.  
  92. .icon-stored:before {
  93. font-family: serif !important;
  94. content: "🏭";
  95. font-weight: bolder !important;
  96. }
  97. `);
  98.  
  99. (function() {
  100. 'use strict';
  101. /** Amout of figures user has already preordered */
  102. let BASE_COUNT = 0;
  103.  
  104. /** Parameter value for detailed list */
  105. const DETAILED_LIST = '0';
  106.  
  107. /** Array of non-MFC figures the user has preordered */
  108. let additionalFigures = JSON.parse(GM_getValue("additionalFigures", "[]"));
  109.  
  110. /** Pips for paid and shipped */
  111. const PAID_PIP = '<span class="item-custompip item-is-paid"><span class="tiny-icon-only icon-dollar"></span></span>';
  112. const SHIPPED_PIP = '<span class="item-custompip item-is-shipped"><span class="tiny-icon-only icon-plane"></span></span>';
  113. const STORED_PIP = '<span class="item-custompip item-is-stored"><span class="tiny-icon-only icon-stored"></span></span>';
  114.  
  115. const ORDERED_COUNT_SELECTOR = '.result-tabs .tab .selected .count';
  116.  
  117. /**
  118. * Converts string to HTML elements
  119. */
  120. const htmlToElement = (html) => {
  121. let template = document.createElement('template');
  122. html = html.trim();
  123. template.innerHTML = `${html}`;
  124. return template.content;
  125. }
  126.  
  127. /**
  128. * Tests if we're viewing the logged in user's preorders page
  129. */
  130. const isUsersPreorderPage = () => {
  131. const urlParams = new URLSearchParams(window.location.search);
  132. const status = urlParams.get('status');
  133. if(status == 1) {
  134. let loggedInUser = document.querySelector('.user-menu .handle');
  135. if(loggedInUser) {
  136. let userLink = loggedInUser.getAttribute('href');
  137. if(userLink === '/session/signup') {
  138. return false;
  139. } else {
  140. let userParam;
  141. const windowLocation = window.location.href;
  142. if(windowLocation.startsWith('https://myfigurecollection.net/profile/')) {
  143. userParam = windowLocation.match(/https:\/\/myfigurecollection\.net\/profile\/([^\/]*)/)[1];
  144. } else {
  145. const urlParams = new URLSearchParams(window.location.search);
  146. userParam = urlParams.get('username');
  147. }
  148. return userParam === userLink.substring('9');
  149. }
  150. } else {
  151. return false;
  152. }
  153. } else {
  154. return false;
  155. }
  156. };
  157.  
  158. /** Helper function to reset out MFC item form */
  159. const resetNonMfcItemForm = () => {
  160. document.getElementById('non-mfc-item__form-title').innerText = 'Add non-MFC figure';
  161. document.getElementById('addNonMfcItem').innerText = 'Add';
  162. document.getElementById('nonMfcItemEditId').value = '';
  163. document.getElementById('nonMfcItemName').value = '';
  164. document.getElementById('nonMfcItemImage').value = '';
  165. document.getElementById('nonMfcItemLink').value = '';
  166. document.getElementById('nonMfcItemReleaseDate').value = '';
  167. document.getElementById('nonMfcItemHasShipped').checked = false;
  168. document.getElementById('nonMfcItemIsStored').checked = false;
  169. document.getElementById('nonMfcItemHasPaid').checked = false;
  170. }
  171.  
  172. const drawAdditionalFigures = () => {
  173. const urlParams = new URLSearchParams(window.location.search);
  174. const groupBy = urlParams.get('groupBy');
  175. const output = urlParams.get('output');
  176. //Clear all additions before redo
  177. const additions = document.getElementById('nonMfcAdditions');
  178. if(additions) {
  179. additions.remove();
  180. }
  181. document.querySelectorAll('.non-mfc-item__icon').forEach(e => e.remove());
  182. document.querySelectorAll('.non-mfc-item__edit').forEach(e => e.remove());
  183. document.querySelectorAll('.non-mfc-item__delete').forEach(e => e.remove());
  184. document.querySelectorAll('.non-mfc-item__year-header').forEach(e => e.remove());
  185. //Create a lookup list of existing date headings and their associated object
  186. //Created so we can splice and keep track of new entries
  187. const dateHeadingsList = [];
  188. if(groupBy == 'releaseDates') {
  189. let dateHeadings;
  190. if(output === '0') {
  191. dateHeadings = document.querySelectorAll('div.results-title');
  192. } else {
  193. dateHeadings = document.querySelectorAll('.item-group-by h3');
  194. }
  195. dateHeadings.forEach((heading) => {
  196. const dateHeadingElement = {};
  197. dateHeadingElement.year = heading.innerText.substring(0,4);
  198. dateHeadingElement.month = heading.innerText.substring(5);
  199. dateHeadingElement.element = heading;
  200. dateHeadingsList.push(dateHeadingElement);
  201. });
  202. }
  203. if(additionalFigures.length > 0) {
  204. //update item count
  205. let totalFigureCount = BASE_COUNT + additionalFigures.length;
  206. document.querySelector(ORDERED_COUNT_SELECTOR).innerText = totalFigureCount;
  207. document.querySelector('.results-count-value').lastChild.nodeValue = `${totalFigureCount} items`;
  208. //Pre-add setup
  209. const addList = document.querySelector('#nonMfcFigureList');
  210. if(groupBy !== 'releaseDates' && !!groupBy) {
  211. if(output === DETAILED_LIST) {
  212. document.querySelector('.results').append(htmlToElement('<div id="nonMfcAdditions" class="results-title">Non-MFC</div>'));
  213. } else {
  214. document.querySelector('.result .item-icons').prepend(htmlToElement('<div class="item-group-by" id="nonMfcAdditions"><h3>Non-MFC</h3></div>'));
  215. }
  216.  
  217. }
  218. //Add icons to page and delete box
  219. additionalFigures.forEach((fig, idx) => {
  220. let linkLine;
  221. const figYear = fig.date.substring(0, 4);
  222. const figMonth = fig.date.substring(5);
  223. let pip = '';
  224. if(fig.hasShipped) {
  225. pip += SHIPPED_PIP;
  226. } else if(fig.isStored) {
  227. pip += STORED_PIP;
  228. } else if(fig.hasPaid) {
  229. pip += PAID_PIP;
  230. }
  231. const figureThumb = `<span class="non-mfc-item__icon item-icon">
  232. <a href="${fig.link}" class="anchor tbx-tooltip item-root-0 item-category-1">
  233. <img src="${fig.image}"/>
  234. ${pip}
  235. </a>
  236. </span>`;
  237. if(output === DETAILED_LIST) {
  238. linkLine = `<div class="result">
  239. <div class="dgst item-dgst item-stamp">
  240. <div class="dgst-wrapper">
  241. <a href="${fig.link}" class="dgst-icon has-flags">
  242. <img src="${fig.image}" alt="${fig.name}">
  243. </a>
  244. <div class="dgst-data">
  245. <div class="dgst-anchor">
  246. <a href="${fig.link}" title="${fig.name}">${fig.name}</a>
  247. </div>
  248. <div class="dgst-meta">
  249. <span class="meta category item-category-1" href="#">Non-MFC</span>
  250. </div>
  251. <div class="dgst-meta">
  252. <span class="meta time">${fig.date}</span>
  253. </div></div></div></div></div>`
  254. }
  255. addList.append(htmlToElement(`<span class='non-mfc-item__icon-container'>
  256. <span class="non-mfc-item__icon non-mfc-item__tray-icon">
  257. <a href="${fig.link}" class="tbx-tooltip item-root-0 item-category-1">
  258. <img src="${fig.image}"/>
  259. </a>
  260. </span>
  261. <a href="#" class='non-mfc-item__edit' title="Edit" data-index="${idx}"><span class="tiny-icon-only icon-pencil-square-o" data-index="${idx}"></span></a>
  262. <a href="#" class='non-mfc-item__delete' title="Delete" data-index="${idx}"><span class="tiny-icon-only icon-trash-o" data-index="${idx}"></span></a>
  263. </span>`));
  264. let toAppend = true;
  265. const yearMonthString = `${figYear}-${figMonth}`;
  266. if(groupBy == 'releaseDates') {
  267. dateHeadingsList.forEach((heading, headingIndex) => {
  268. if(toAppend) {
  269. const headingYear = heading.year;
  270. const headingMonth = heading.month;
  271. if(figYear == headingYear && figMonth == headingMonth) {
  272. if(output === DETAILED_LIST) {
  273. heading.element.after(htmlToElement(linkLine));
  274. } else {
  275. heading.element.after(htmlToElement(figureThumb));
  276. }
  277. toAppend = false;
  278. } else if(new Date(figYear, figMonth, 1) > new Date(headingYear, headingMonth, 1)) {
  279. if(output === DETAILED_LIST) {
  280. const newHeading = `<div class="results-title non-mfc-item__year-header" id='nhi_${yearMonthString}'>${yearMonthString}</div>`
  281. heading.element.before(htmlToElement(newHeading + linkLine));
  282. } else {
  283. const newHeading = `<div class="item-group-by"><h3 class="non-mfc-item__year-header" id='nhi_${yearMonthString}'>${yearMonthString}</h3></div>`
  284. heading.element.parentElement.parentElement.insertBefore(htmlToElement(newHeading), heading.element.parentElement);
  285. document.querySelector(`#nhi_${yearMonthString}`).after(htmlToElement(figureThumb));
  286. }
  287. dateHeadingsList.splice(
  288. headingIndex,
  289. 0,
  290. {
  291. year: figYear,
  292. month: figMonth,
  293. element: document.querySelector(`#nhi_${yearMonthString}`)
  294. }
  295. );
  296. toAppend = false;
  297. }
  298. }
  299. });
  300. //Append if trailing
  301. if(toAppend) {
  302. if(output === DETAILED_LIST) {
  303. const newHeading = `<div class="results-title non-mfc-item__year-header" id='nhi_${yearMonthString}'>${yearMonthString}</div>`
  304. document.querySelector('.results').append(htmlToElement('<div class="item-group-by">' + newHeading + linkLine + '</div>'));
  305. } else {
  306. const newHeading = `<h3 class="non-mfc-item__year-header" id='nhi_${yearMonthString}'>${yearMonthString}</h3>`
  307. document.querySelector('.item-group-by:last-child').after(htmlToElement('<div class="item-group-by">' + newHeading + figureThumb + '</div>'));
  308. }
  309. dateHeadingsList.push({
  310. year: figYear,
  311. month: figMonth,
  312. element: document.querySelector(`#nhi_${yearMonthString}`)
  313. });
  314. }
  315. } else if(!!groupBy) {
  316. if(output === DETAILED_LIST) {
  317. document.getElementById('nonMfcAdditions').after(htmlToElement(linkLine))
  318. } else {
  319. document.querySelector('#nonMfcAdditions h3').after(htmlToElement(figureThumb));
  320. }
  321. } else {
  322. if(output === DETAILED_LIST) {
  323. document.querySelector('.results').append(htmlToElement(linkLine));
  324. } else {
  325. document.querySelector('.result .item-icons').prepend(htmlToElement(figureThumb));
  326. }
  327. }
  328. });
  329. document.querySelectorAll('.non-mfc-item__delete').forEach(ele => {
  330. ele.addEventListener('click', (evt) => {
  331. evt.preventDefault();
  332. if(confirm('Delete this figure?')) {
  333. additionalFigures.splice(evt.target.getAttribute('data-index'), 1);
  334. additionalFigures.sort((a, b) => (a.date < b.date) ? 1 : -1);
  335. GM_setValue("additionalFigures", JSON.stringify(additionalFigures));
  336. drawAdditionalFigures();
  337. resetNonMfcItemForm();
  338. }
  339. });
  340. });
  341. document.querySelectorAll('.non-mfc-item__edit').forEach(ele => {
  342. ele.addEventListener('click', (evt) => {
  343. evt.preventDefault();
  344. const itemIndex = evt.target.getAttribute('data-index');
  345. const figToEdit = additionalFigures[itemIndex];
  346. document.getElementById('non-mfc-item__form-title').innerText = `Editing ${figToEdit.name}`;
  347. document.getElementById('addNonMfcItem').innerText = 'Edit';
  348. document.getElementById('nonMfcItemEditId').value = itemIndex;
  349. document.getElementById('nonMfcItemName').value = figToEdit.name;
  350. document.getElementById('nonMfcItemImage').value = figToEdit.image;
  351. document.getElementById('nonMfcItemLink').value = figToEdit.link;
  352. document.getElementById('nonMfcItemReleaseDate').value = figToEdit.date;
  353. document.getElementById('nonMfcItemHasShipped').checked = figToEdit.hasShipped;
  354. document.getElementById('nonMfcItemIsStored').checked = figToEdit.isStored;
  355. document.getElementById('nonMfcItemHasPaid').checked = figToEdit.hasPaid;
  356. window.scrollTo(
  357. document.getElementById('non-mfc-item__section').offsetLeft,
  358. document.getElementById('non-mfc-item__section').offsetTop - 100
  359. );
  360. });
  361. });
  362. }
  363. }
  364.  
  365. // Initialise non-MFC
  366. BASE_COUNT = parseInt(document.querySelector(ORDERED_COUNT_SELECTOR).innerText);
  367. if(isUsersPreorderPage()) {
  368. const addSection = `<section id='non-mfc-item__section'>
  369. <h2 id='non-mfc-item__form-title'>Add non-MFC figure</h2>
  370. <div class='form'>
  371. <input type='hidden' id='nonMfcItemEditId' value=''/>
  372. <div class='bigchar form-field'>
  373. <div class='form-label'>Figure name</div>
  374. <div class='form-input'><input type='text' id='nonMfcItemName'/></div>
  375. </div>
  376. <div class='bigchar form-field'>
  377. <div class='form-label'>Image URL *</div>
  378. <div class='form-input'><input type='text' id='nonMfcItemImage'/></div>
  379. </div>
  380. <div class='bigchar form-field'>
  381. <div class='form-label'>Item link *</div>
  382. <div class='form-input'><input type='text' id='nonMfcItemLink'/></div>
  383. </div>
  384. <div class='bigchar form-field'>
  385. <div class='form-label'>Release date (YYYY-MM) *</div>
  386. <div class='form-input'><input type='text' id='nonMfcItemReleaseDate'/></div>
  387. </div>
  388. <div class="checkbox form-field">
  389. <div class="form-input">
  390. <input id="nonMfcItemHasPaid" type="checkbox">&nbsp;<label for="nonMfcItemHasPaid">Paid?</label>
  391. </div>
  392. </div>
  393. <div class="checkbox form-field">
  394. <div class="form-input">
  395. <input id="nonMfcItemIsStored" type="checkbox">&nbsp;<label for="nonMfcItemIsStored">Stored?</label>
  396. </div>
  397. </div>
  398. <div class="checkbox form-field">
  399. <div class="form-input">
  400. <input id="nonMfcItemHasShipped" type="checkbox">&nbsp;<label for="nonMfcItemHasShipped">Shipped?</label>
  401. </div>
  402. </div>
  403. <div class='form-field'>
  404. <div class='form-input'>
  405. <button id='addNonMfcItem'>Add</button>
  406. <button id='cancelNonMfcItem'>Cancel</button>
  407. </div>
  408. </div>
  409. </div>
  410. </section>
  411. <section>
  412. <h2>Added non-MFC figures</h2>
  413. <div class='form'>
  414. <div id='nonMfcFigureList' class='item-icons'>
  415. </div>
  416. </div>
  417. </section>`;
  418. const sidebar = document.querySelectorAll('#side section');
  419. const lastSidebarSection = sidebar[sidebar.length - 1]
  420. lastSidebarSection.after(htmlToElement(addSection));
  421. drawAdditionalFigures();
  422. document.getElementById('addNonMfcItem').addEventListener('click', (evt) => {
  423. evt.preventDefault();
  424. const name = document.getElementById('nonMfcItemName').value || 'Non-MFC figure';
  425. const image = document.getElementById('nonMfcItemImage').value;
  426. const link = document.getElementById('nonMfcItemLink').value;
  427. const date = document.getElementById('nonMfcItemReleaseDate').value;
  428. const editId = document.getElementById('nonMfcItemEditId').value;
  429. const hasShipped = document.getElementById('nonMfcItemHasShipped').checked;
  430. const isStored = document.getElementById('nonMfcItemIsStored').checked;
  431. const hasPaid = document.getElementById('nonMfcItemHasPaid').checked;
  432. if(image && link && date && name) {
  433. if(date.match(/^\d{4}-0[1-9]|1[0-2]$/)) {
  434. if(editId) {
  435. additionalFigures[editId] = {
  436. name,
  437. image,
  438. link,
  439. date,
  440. hasPaid,
  441. isStored,
  442. hasShipped
  443. }
  444. } else {
  445. additionalFigures.push({
  446. name,
  447. image,
  448. link,
  449. date,
  450. hasPaid,
  451. isStored,
  452. hasShipped
  453. });
  454. }
  455. additionalFigures.sort((a, b) => (a.date < b.date) ? 1 : -1);
  456. GM_setValue('additionalFigures', JSON.stringify(additionalFigures));
  457. drawAdditionalFigures();
  458. resetNonMfcItemForm();
  459. } else {
  460. alert('Please enter a valid date in YYYY-MM format');
  461. }
  462. } else {
  463. alert('Please fill in all mandatory fields');
  464. }
  465. });
  466. const cancelButton = document.getElementById('cancelNonMfcItem');
  467. cancelButton.addEventListener('click', (evt) => {
  468. evt.preventDefault();
  469. if(confirm('Are you sure you want to cancel?')) {
  470. resetNonMfcItemForm();
  471. }
  472. });
  473. }
  474.  
  475. })();