Add non-MFC figure

Track preordered non-MFC items on collection screen

目前为 2023-07-08 提交的版本,查看 最新版本

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