Add non-MFC figure

Track preordered non-MFC items on collection screen

当前为 2024-09-03 提交的版本,查看 最新版本

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