Greasy Fork is available in English.

Markethunt plugin for Mousehunt

Adds a price chart and Markethunt integration to the MH marketplace screen.

  1. // ==UserScript==
  2. // @name Markethunt plugin for Mousehunt
  3. // @author Program
  4. // @namespace https://greasyfork.org/en/users/886222-program
  5. // @license MIT
  6. // @version 1.7.0
  7. // @description Adds a price chart and Markethunt integration to the MH marketplace screen.
  8. // @resource jq_confirm_css https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.css
  9. // @resource jq_toast_css https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/highcharts/9.3.2/highstock.min.js
  14. // @include https://www.mousehuntgame.com/*
  15. // @grant GM_addStyle
  16. // @grant GM_getResourceText
  17. //
  18. // ==/UserScript==
  19.  
  20. const markethuntDomain = 'markethunt.win';
  21. const markethuntApiDomain = 'api.markethunt.win';
  22.  
  23. MutationObserver =
  24. window.MutationObserver ||
  25. window.WebKitMutationObserver ||
  26. window.MozMutationObserver;
  27.  
  28. function sleep(ms) {
  29. return new Promise(resolve => setTimeout(resolve, ms));
  30. }
  31.  
  32. const RoundToIntLocaleStringOpts = {
  33. maximumFractionDigits: 0
  34. }
  35.  
  36. const isDarkMode = (() => {
  37. let isDark = null;
  38.  
  39. return () => {
  40. if (isDark == null) {
  41. isDark = !!getComputedStyle(document.documentElement).getPropertyValue('--mhdm-white');
  42. }
  43.  
  44. return isDark;
  45. }
  46. })();
  47.  
  48. /*******************************
  49. *
  50. * Plugin settings
  51. *
  52. *******************************/
  53.  
  54. class SettingsController {
  55. // TODO: make settings property private and convert init into static initializer once greasyfork adds support
  56. static settings;
  57.  
  58. static init() {
  59. let settingsObj = {};
  60.  
  61. if (localStorage.markethuntSettings !== undefined) {
  62. settingsObj = JSON.parse(localStorage.markethuntSettings);
  63. }
  64.  
  65. this.settings = new Proxy(settingsObj, {
  66. set(obj, prop, value) {
  67. obj[prop] = value;
  68. localStorage.markethuntSettings = JSON.stringify(obj);
  69. return true;
  70. }
  71. });
  72. }
  73.  
  74. static getStartChartAtZero() {
  75. if (this.settings.startChartAtZero === undefined) {
  76. return false;
  77. } else {
  78. return this.settings.startChartAtZero;
  79. }
  80. }
  81.  
  82. static setStartChartAtZero(value) {
  83. this.settings.startChartAtZero = value;
  84. }
  85.  
  86. static getEnablePortfolioButtons() {
  87. if (this.settings.enablePortfolioButtons === undefined) {
  88. return true;
  89. } else {
  90. return this.settings.enablePortfolioButtons;
  91. }
  92. }
  93.  
  94. static setEnablePortfolioButtons(value) {
  95. this.settings.enablePortfolioButtons = value;
  96. }
  97.  
  98. static getEnableFloatingVolumeLabels() {
  99. if (this.settings.enableFloatingVolumeLabels === undefined) {
  100. return false;
  101. } else {
  102. return this.settings.enableFloatingVolumeLabels;
  103. }
  104. }
  105.  
  106. static setEnableFloatingVolumeLabels(value) {
  107. this.settings.enableFloatingVolumeLabels = value;
  108. }
  109.  
  110. static getEnableChartAnimation() {
  111. if (this.settings.enableChartAnimation === undefined) {
  112. return true;
  113. } else {
  114. return this.settings.enableChartAnimation;
  115. }
  116. }
  117.  
  118. static setEnableChartAnimation(value) {
  119. this.settings.enableChartAnimation = value;
  120. }
  121. }
  122.  
  123. SettingsController.init();
  124.  
  125. function openPluginSettings() {
  126. $.alert({
  127. title: 'Markethunt Plugin Settings',
  128. content: `
  129. <div id="markethunt-settings-container">
  130. <h1>Chart settings</h1>
  131. <label for="checkbox-start-chart-at-zero" class="cl-switch markethunt-settings-row">
  132. <div class="markethunt-settings-row-input">
  133. <input id="checkbox-start-chart-at-zero" type="checkbox" ${SettingsController.getStartChartAtZero() ? 'checked' : ''}>
  134. <span class="switcher"></span>
  135. </div>
  136. <div class="label markethunt-settings-row-description">
  137. <b>Y-axis starts at 0</b><br>
  138. Make the Y-axis start at 0 gold/SB
  139. </div>
  140. </label>
  141. <label for="checkbox-enable-floating-volume-labels" class="cl-switch markethunt-settings-row">
  142. <div class="markethunt-settings-row-input">
  143. <input id="checkbox-enable-floating-volume-labels" type="checkbox" ${SettingsController.getEnableFloatingVolumeLabels() ? 'checked' : ''}>
  144. <span class="switcher"></span>
  145. </div>
  146. <div class="label markethunt-settings-row-description">
  147. <b>Volume labels</b><br>
  148. Place floating labels indicating volume amount on the left side of the chart
  149. </div>
  150. </label>
  151. <label for="checkbox-enable-chart-animation" class="cl-switch markethunt-settings-row">
  152. <div class="markethunt-settings-row-input">
  153. <input id="checkbox-enable-chart-animation" type="checkbox" ${SettingsController.getEnableChartAnimation() ? 'checked' : ''}>
  154. <span class="switcher"></span>
  155. </div>
  156. <div class="label markethunt-settings-row-description">
  157. <b>Chart animation</b><br>
  158. Enable chart animations
  159. </div>
  160. </label>
  161. <h1>Other settings</h1>
  162. <label for="checkbox-enable-portfolio-buttons" class="cl-switch markethunt-settings-row">
  163. <div class="markethunt-settings-row-input">
  164. <input id="checkbox-enable-portfolio-buttons" type="checkbox" ${SettingsController.getEnablePortfolioButtons() ? 'checked' : ''}>
  165. <span class="switcher"></span>
  166. </div>
  167. <div class="label markethunt-settings-row-description">
  168. <b>Portfolio quick-add buttons</b><br>
  169. Place "Add to portfolio" buttons in your marketplace history and journal log
  170. </div>
  171. </label>
  172. </div>
  173. `,
  174. boxWidth: '450px',
  175. useBootstrap: false,
  176. closeIcon: true,
  177. draggable: true,
  178. onOpen: function(){
  179. const startChartAtZeroCheckbox = document.getElementById("checkbox-start-chart-at-zero");
  180. startChartAtZeroCheckbox.addEventListener('change', function(event) {
  181. SettingsController.setStartChartAtZero(event.currentTarget.checked);
  182. });
  183.  
  184. const enablePortfolioButtonsCheckbox = document.getElementById("checkbox-enable-portfolio-buttons");
  185. enablePortfolioButtonsCheckbox.addEventListener('change', function(event) {
  186. SettingsController.setEnablePortfolioButtons(event.currentTarget.checked);
  187. });
  188.  
  189. const enableFloatingVolumeLabelsCheckbox = document.getElementById("checkbox-enable-floating-volume-labels");
  190. enableFloatingVolumeLabelsCheckbox.addEventListener('change', function(event) {
  191. SettingsController.setEnableFloatingVolumeLabels(event.currentTarget.checked);
  192. });
  193.  
  194. const enableChartAnimationCheckbox = document.getElementById("checkbox-enable-chart-animation");
  195. enableChartAnimationCheckbox.addEventListener('change', function(event) {
  196. SettingsController.setEnableChartAnimation(event.currentTarget.checked);
  197. });
  198. }
  199. });
  200. }
  201.  
  202. /*******************************
  203. *
  204. * Chart functions
  205. *
  206. *******************************/
  207.  
  208. // chart vars
  209. const UtcTimezone = "T00:00:00+00:00"
  210.  
  211. // style
  212. const primaryLineColor = "#4f52aa";
  213. const secondaryLineColor = "#b91a05";
  214. const sbiLineColor = "#00c000"
  215. const volumeColor = "#51cda0";
  216. const volumeLabelColor = '#3ab28a';
  217.  
  218. const eventBandColor = "#f2f2f2";
  219. const eventBandFontColor = "#888888"; // recommend to have same or close color as yGridLineColor for visual clarity
  220. const xGridLineColor = "#bbbbbb";
  221. const yGridLineColor = "#aaaaaa";
  222. const yGridLineColorLighter = "#dddddd";
  223. const axisLabelColor = "#444444";
  224. const crosshairColor = "#252525";
  225.  
  226. // dark mode style overrides
  227. const darkMode = {
  228. backgroundColor: "#222222",
  229. eventBandColor: "#303030",
  230. eventBandFontColor: "#909090",
  231. primaryLineColor: "#9e8dfc",
  232. crosshairColor: "#e0e0e0"
  233. }
  234.  
  235. const chartFont = "tahoma,arial,sans-serif";
  236.  
  237. // set global opts
  238. Highcharts.setOptions({
  239. chart: {
  240. style: {
  241. fontFamily: chartFont,
  242. },
  243. spacingLeft: 0,
  244. spacingRight: 5,
  245. spacingTop: 7,
  246. spacingBottom: 6,
  247. },
  248. lang: {
  249. rangeSelectorZoom :""
  250. },
  251. plotOptions: {
  252. series: {
  253. showInLegend: true,
  254. },
  255. },
  256. // must keep scrollbar enabled for dynamic scrolling, so hide the scrollbar instead
  257. scrollbar: {
  258. height: 0,
  259. buttonArrowColor: "#ffffff00",
  260. },
  261. title: {
  262. enabled: false,
  263. },
  264. credits: {
  265. enabled: false,
  266. },
  267. rangeSelector: {
  268. buttonPosition: {
  269. y: 5,
  270. },
  271. inputEnabled: false,
  272. labelStyle: {
  273. color: axisLabelColor,
  274. },
  275. verticalAlign: 'top',
  276. x: -5.5,
  277. },
  278. legend: {
  279. align: 'right',
  280. verticalAlign: 'top',
  281. y: -23,
  282. padding: 0,
  283. itemStyle: {
  284. color: '#000000',
  285. fontSize: "13px",
  286. },
  287. },
  288. tooltip: {
  289. animation: false,
  290. shared: true,
  291. split: false,
  292. headerFormat: '<span style="font-size: 11px; font-weight: bold">{point.key}</span><br/>',
  293. backgroundColor: 'rgba(255, 255, 255, 1)',
  294. hideDelay: 0, // makes tooltip feel more responsive when crossing gap between plots
  295. style: {
  296. color: '#000000',
  297. fontSize: '11px',
  298. fontFamily: chartFont,
  299. }
  300. },
  301. navigator: {
  302. height: 25,
  303. margin: 0,
  304. maskInside: false,
  305. enabled: false,
  306. },
  307. xAxis: {
  308. tickColor: xGridLineColor,
  309. gridLineColor: xGridLineColor,
  310. labels: {
  311. style: {
  312. color: axisLabelColor,
  313. fontSize: '11px',
  314. }
  315. }
  316. },
  317. yAxis: {
  318. gridLineColor: yGridLineColor,
  319. labels: {
  320. style: {
  321. color: axisLabelColor,
  322. fontSize: '11px',
  323. },
  324. y: 3,
  325. }
  326. }
  327. });
  328.  
  329. function setDarkThemeGlobalOpts() {
  330. Highcharts.setOptions({
  331. chart: {
  332. backgroundColor: darkMode.backgroundColor
  333. },
  334. legend: {
  335. itemStyle: {
  336. color: '#e0e0e0'
  337. },
  338. itemHoverStyle: {
  339. color: '#f0f0f0'
  340. },
  341. itemHiddenStyle: {
  342. color: '#777777'
  343. },
  344. title: {
  345. style: {
  346. color: '#c0c0c0'
  347. }
  348. }
  349. },
  350. xAxis: {
  351. gridLineColor: '#707070',
  352. labels: {
  353. style: {
  354. color: '#e0e0e0'
  355. }
  356. },
  357. lineColor: '#707070',
  358. minorGridLineColor: '#505050',
  359. tickColor: '#707070',
  360. },
  361. yAxis: {
  362. gridLineColor: '#707070',
  363. labels: {
  364. style: {
  365. color: '#e0e0e0'
  366. }
  367. },
  368. lineColor: '#707070',
  369. minorGridLineColor: '#505050',
  370. tickColor: '#707070',
  371. },
  372. tooltip: {
  373. backgroundColor: '#000000',
  374. style: {
  375. color: '#f0f0f0'
  376. }
  377. },
  378. rangeSelector: {
  379. buttonTheme: {
  380. fill: '#444444',
  381. stroke: '#000000',
  382. style: {
  383. color: '#cccccc'
  384. },
  385. states: {
  386. hover: {
  387. fill: '#707070',
  388. stroke: '#000000',
  389. style: {
  390. color: 'white'
  391. }
  392. },
  393. select: {
  394. fill: '#000000',
  395. stroke: '#000000',
  396. style: {
  397. color: 'white'
  398. }
  399. }
  400. }
  401. },
  402. },
  403. });
  404. }
  405.  
  406. function UtcIsoDateToMillis(dateStr) {
  407. return (new Date(dateStr + UtcTimezone)).getTime();
  408. }
  409.  
  410. function formatSISuffix(num, decimalPlaces) {
  411. const suffixes = ["", "K", "M", "B"];
  412. let order = Math.max(Math.floor(Math.log(num) / Math.log(1000)), 0);
  413. if (order > suffixes.length - 1) {
  414. order = suffixes.length - 1;
  415. }
  416. let significand = num / Math.pow(1000, order);
  417. return significand.toFixed(decimalPlaces) + suffixes[order];
  418. }
  419.  
  420. function eventBand(IsoStrFrom, IsoStrTo, labelText) {
  421. return {
  422. from: UtcIsoDateToMillis(IsoStrFrom),
  423. to: UtcIsoDateToMillis(IsoStrTo),
  424. color: isDarkMode() ? darkMode.eventBandColor : eventBandColor,
  425. label: {
  426. text: labelText,
  427. rotation: 270,
  428. textAlign: 'right',
  429. y: 5, // pixels from top of chart
  430. x: 4, // fix slight centering issue
  431. style: {
  432. color: isDarkMode() ? darkMode.eventBandFontColor : eventBandFontColor,
  433. fontSize: '12px',
  434. fontFamily: chartFont,
  435. },
  436. },
  437. }
  438. }
  439.  
  440. function updateEventData() {
  441. $.getJSON(`https://${markethuntApiDomain}/events?plugin_ver=${GM_info.script.version}`, function (response) {
  442. localStorage.markethuntEventDatesV2 = JSON.stringify(response);
  443. localStorage.markethuntEventDatesV2LastRetrieval = Date.now();
  444. });
  445. }
  446.  
  447. function renderChartWithItemId(itemId, containerId, forceRender = false) {
  448. const containerElement = document.getElementById(containerId);
  449.  
  450. if (forceRender === false && containerElement.dataset.lastRendered) {
  451. return;
  452. }
  453.  
  454. itemId = Number(itemId);
  455. let eventData = [];
  456.  
  457. if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
  458. JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));
  459.  
  460. if (Date.now() - Number(localStorage.markethuntEventDatesV2LastRetrieval) > 2 * 86400 * 1000) {
  461. updateEventData();
  462. }
  463. } else {
  464. updateEventData();
  465. }
  466.  
  467. function renderChart(response) {
  468. // set HUD
  469. if (response.market_data.length > 0) {
  470. const newestPrice = response.market_data[response.market_data.length - 1];
  471. const utcTodayMillis = UtcIsoDateToMillis(new Date().toISOString().substring(0, 10));
  472.  
  473. const priceDisplay = document.getElementById("infoboxPrice");
  474. const sbPriceDisplay = document.getElementById("infoboxSbPrice");
  475. const tradeVolDisplay = document.getElementById("infoboxTradevol");
  476. const goldVolDisplay = document.getElementById("infoboxGoldvol");
  477. const weeklyVolDisplay = document.getElementById("infobox7dTradevol");
  478. const weeklyGoldVolDisplay = document.getElementById("infobox7dGoldvol");
  479.  
  480. // set gold price
  481. priceDisplay.innerHTML = newestPrice.price.toLocaleString();
  482.  
  483. // set sb price
  484. try {
  485. let sbPriceText;
  486. let sbPrice = newestPrice.sb_price;
  487. if (sbPrice >= 100) {
  488. sbPriceText = Math.round(sbPrice).toLocaleString();
  489. } else {
  490. sbPriceText = sbPrice.toFixed(2).toLocaleString();
  491. }
  492. sbPriceDisplay.innerHTML = sbPriceText;
  493. } catch (e) {
  494. // do nothing
  495. }
  496.  
  497. // set yesterday's trade volume
  498. let volText = '0';
  499. if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
  500. volText = newestPrice.volume.toLocaleString();
  501. }
  502. tradeVolDisplay.innerHTML = volText;
  503.  
  504. // set yesterday's gold volume
  505. let goldVolText = '0';
  506. if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
  507. goldVolText = formatSISuffix(newestPrice.volume * newestPrice.price, 2);
  508. }
  509. goldVolDisplay.innerHTML = goldVolText;
  510.  
  511. // set last week's trade volume
  512. let weeklyVolText = response.market_data.reduce(function(sum, dataPoint) {
  513. if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
  514. return sum + (dataPoint.volume !== null ? dataPoint.volume : 0);
  515. } else {
  516. return sum;
  517. }
  518. }, 0);
  519. weeklyVolDisplay.innerHTML = weeklyVolText.toLocaleString();
  520.  
  521. // set last week's gold volume
  522. let weeklyGoldVol = response.market_data.reduce(function(sum, dataPoint) {
  523. if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
  524. return sum + (dataPoint.volume !== null ? dataPoint.volume * dataPoint.price : 0);
  525. } else {
  526. return sum;
  527. }
  528. }, 0);
  529. weeklyGoldVolDisplay.innerHTML = (weeklyGoldVol === 0) ? '0' : formatSISuffix(weeklyGoldVol, 2);
  530. }
  531.  
  532. // process data for highcharts
  533. var dailyPrices = [];
  534. var dailyVolumes = [];
  535. var dailySbPrices = [];
  536. for (var i = 0; i < response.market_data.length; i++) {
  537. dailyPrices.push([
  538. UtcIsoDateToMillis(response.market_data[i].date),
  539. Number(response.market_data[i].price)
  540. ]);
  541. dailyVolumes.push([
  542. UtcIsoDateToMillis(response.market_data[i].date),
  543. Number(response.market_data[i].volume)
  544. ]);
  545. dailySbPrices.push([
  546. UtcIsoDateToMillis(response.market_data[i].date),
  547. Number(response.market_data[i].sb_price)
  548. ]);
  549. }
  550.  
  551. if (isDarkMode()) {
  552. setDarkThemeGlobalOpts();
  553. }
  554.  
  555. // Create the chart
  556. let chart = new Highcharts.stockChart(containerId, {
  557. chart: {
  558. // zoom animations
  559. animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
  560. },
  561. plotOptions: {
  562. series: {
  563. // initial animation
  564. animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
  565. dataGrouping: {
  566. enabled: itemId === 114,
  567. units: [['day', [1]], ['week', [1]]],
  568. groupPixelWidth: 3,
  569. },
  570. },
  571. },
  572. rangeSelector: {
  573. buttons: [
  574. {
  575. type: 'month',
  576. count: 1,
  577. text: '1M'
  578. }, {
  579. type: 'month',
  580. count: 3,
  581. text: '3M'
  582. }, {
  583. type: 'month',
  584. count: 6,
  585. text: '6M'
  586. }, {
  587. type: 'year',
  588. count: 1,
  589. text: '1Y',
  590. }, {
  591. type: 'all',
  592. text: 'All'
  593. },
  594. ],
  595. selected: 3,
  596. },
  597. legend: {
  598. enabled: true
  599. },
  600. tooltip: {
  601. xDateFormat: '%b %e, %Y',
  602. },
  603. series: [
  604. {
  605. name: 'Average price',
  606. id: 'dailyPrice',
  607. data: dailyPrices,
  608. lineWidth: 1.5,
  609. states: {
  610. hover: {
  611. lineWidthPlus: 0,
  612. halo: false, // disable translucent halo on marker hover
  613. }
  614. },
  615. yAxis: 0,
  616. color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
  617. marker: {
  618. states: {
  619. hover: {
  620. lineWidth: 0,
  621. }
  622. },
  623. },
  624. tooltip: {
  625. pointFormatter: function() {
  626. return `<span style="color:${this.color}">\u25CF</span>`
  627. + ` ${this.series.name}:`
  628. + ` <b>${this.y.toLocaleString()}g</b><br/>`;
  629. },
  630. },
  631. zIndex: 1,
  632. }, {
  633. name: 'Volume',
  634. type: 'column',
  635. data: dailyVolumes,
  636. pointPadding: 0, // disable point and group padding to simulate column area chart
  637. groupPadding: 0,
  638. yAxis: 2,
  639. color: volumeColor,
  640. tooltip: {
  641. pointFormatter: function() {
  642. let volumeAmtText = this.y !== 0 ? this.y.toLocaleString() : 'n/a';
  643. return `<span style="color:${this.color}">\u25CF</span>`
  644. + ` ${this.series.name}:`
  645. + ` <b>${volumeAmtText}</b><br/>`;
  646. },
  647. },
  648. zIndex: 0,
  649. }, {
  650. name: 'SB Price',
  651. id: 'sbi',
  652. data: dailySbPrices,
  653. visible: false,
  654. lineWidth: 1.5,
  655. states: {
  656. hover: {
  657. lineWidthPlus: 0,
  658. halo: false, // disable translucent halo on marker hover
  659. }
  660. },
  661. yAxis: 1,
  662. color: sbiLineColor,
  663. marker: {
  664. states: {
  665. hover: {
  666. lineWidth: 0,
  667. }
  668. },
  669. },
  670. tooltip: {
  671. pointFormatter: function() {
  672. let sbiText;
  673.  
  674. if (this.y >= 1000) {
  675. sbiText = Math.round(this.y).toLocaleString();
  676. } else if (this.y >= 100) {
  677. sbiText = this.y.toFixed(1).toLocaleString();
  678. } else if (this.y >= 10) {
  679. sbiText = this.y.toFixed(2).toLocaleString();
  680. } else {
  681. sbiText = this.y.toFixed(3).toLocaleString();
  682. }
  683. return `<span style="color:${this.color}">\u25CF</span>`
  684. + ` SB Index:`
  685. + ` <b>${sbiText} SB</b><br/>`;
  686. },
  687. },
  688. zIndex: 2,
  689. },
  690. ],
  691. yAxis: [
  692. {
  693. min: SettingsController.getStartChartAtZero() ? 0 : null,
  694. labels: {
  695. formatter: function() {
  696. return this.value.toLocaleString() + 'g';
  697. },
  698. x: -8,
  699. },
  700. showLastLabel: true, // show label at top of chart
  701. crosshair: {
  702. dashStyle: 'ShortDot',
  703. color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
  704. },
  705. opposite: false,
  706. alignTicks: false, // disabled, otherwise autoranger will create too large a Y-window
  707. }, {
  708. min: SettingsController.getStartChartAtZero() ? 0 : null,
  709. gridLineWidth: 0,
  710. labels: {
  711. formatter: function() {
  712. return this.value.toLocaleString() + ' SB';
  713. },
  714. x: 5,
  715. },
  716. showLastLabel: true, // show label at top of chart
  717. opposite: true,
  718. alignTicks: false,
  719. }, {
  720. top: '75%',
  721. height: '25%',
  722. offset: 0,
  723. min: 0,
  724. opposite: false,
  725. tickPixelInterval: 35,
  726. allowDecimals: false,
  727. alignTicks: false,
  728. gridLineWidth: 0,
  729. labels: {
  730. enabled: SettingsController.getEnableFloatingVolumeLabels(),
  731. align: 'left',
  732. x: 0,
  733. style: {
  734. color: volumeLabelColor,
  735. },
  736. },
  737. showLastLabel: true,
  738. showFirstLabel: false,
  739. }],
  740. xAxis: {
  741. type: 'datetime',
  742. ordinal: false, // show continuous x axis if dates are missing
  743. plotBands: eventData,
  744. crosshair: {
  745. dashStyle: 'ShortDot',
  746. color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
  747. },
  748. dateTimeLabelFormats:{
  749. day: '%b %e',
  750. week: '%b %e, \'%y',
  751. month: '%b %Y',
  752. year: '%Y'
  753. },
  754. tickPixelInterval: 120,
  755. },
  756. });
  757.  
  758. containerElement.dataset.lastRendered = Date.now().toString();
  759. }
  760.  
  761. $.getJSON(`https://${markethuntApiDomain}/items/${itemId}?plugin_ver=${GM_info.script.version}`, function (response) {
  762. renderChart(response);
  763. });
  764. }
  765.  
  766. function renderStockChartWithItemId(itemId, containerId, forceRender = false) {
  767. const containerElement = document.getElementById(containerId);
  768.  
  769. if (forceRender === false && containerElement.dataset.lastRendered) {
  770. return;
  771. }
  772.  
  773. itemId = Number(itemId);
  774. let eventData = [];
  775.  
  776. if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
  777. JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));
  778. }
  779.  
  780. function renderStockChart(response) {
  781. const bid_data = [];
  782. const ask_data = [];
  783. const supply_data = [];
  784.  
  785. response.stock_data.forEach(x => {
  786. bid_data.push([x.timestamp, x.bid]);
  787. ask_data.push([x.timestamp, x.ask]);
  788. supply_data.push([x.timestamp, x.supply]);
  789. })
  790.  
  791. if (isDarkMode()) {
  792. setDarkThemeGlobalOpts();
  793. }
  794.  
  795. // Create the chart
  796. let chart = new Highcharts.stockChart(containerId, {
  797. chart: {
  798. // zoom animations
  799. animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
  800. },
  801. plotOptions: {
  802. series: {
  803. // initial animation
  804. animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
  805. dataGrouping: {
  806. enabled: true,
  807. units: [['hour', [2, 4, 6]], ['day', [1]], ['week', [1]]],
  808. groupPixelWidth: 2,
  809. dateTimeLabelFormats: {
  810. hour: ['%b %e, %Y %H:%M UTC', '%b %e, %Y %H:%M UTC'],
  811. day: ['%b %e, %Y']
  812. }
  813. },
  814. },
  815. },
  816. rangeSelector: {
  817. buttons: [
  818. {
  819. type: 'day',
  820. count: 7,
  821. text: '7D'
  822. }, {
  823. type: 'month',
  824. count: 1,
  825. text: '1M'
  826. }, {
  827. type: 'month',
  828. count: 3,
  829. text: '3M'
  830. }, {
  831. type: 'month',
  832. count: 6,
  833. text: '6M'
  834. }, {
  835. type: 'year',
  836. count: 1,
  837. text: '1Y'
  838. }, {
  839. type: 'all',
  840. text: 'All'
  841. },
  842. ],
  843. selected: 1,
  844. },
  845. legend: {
  846. enabled: true
  847. },
  848. tooltip: {
  849. xDateFormat: '%b %e, %Y %H:%M UTC',
  850. },
  851. series: [
  852. {
  853. name: 'Ask',
  854. id: 'ask',
  855. data: ask_data,
  856. lineWidth: 1.5,
  857. states: {
  858. hover: {
  859. lineWidthPlus: 0,
  860. halo: false, // disable translucent halo on marker hover
  861. }
  862. },
  863. yAxis: 0,
  864. color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
  865. marker: {
  866. states: {
  867. hover: {
  868. lineWidth: 0,
  869. }
  870. },
  871. },
  872. tooltip: {
  873. pointFormatter: function() {
  874. return `<span style="color:${this.color}">\u25CF</span>`
  875. + ` ${this.series.name}:`
  876. + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
  877. },
  878. },
  879. zIndex: 1,
  880. }, {
  881. name: 'Bid',
  882. id: 'bid',
  883. data: bid_data,
  884. lineWidth: 1.5,
  885. states: {
  886. hover: {
  887. lineWidthPlus: 0,
  888. halo: false, // disable translucent halo on marker hover
  889. }
  890. },
  891. yAxis: 0,
  892. color: secondaryLineColor,
  893. marker: {
  894. states: {
  895. hover: {
  896. lineWidth: 0,
  897. }
  898. },
  899. },
  900. tooltip: {
  901. pointFormatter: function() {
  902. return `<span style="color:${this.color}">\u25CF</span>`
  903. + ` ${this.series.name}:`
  904. + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
  905. },
  906. },
  907. zIndex: 2,
  908. }, {
  909. name: 'Supply',
  910. id: 'supply',
  911. data: supply_data,
  912. type: 'area',
  913. lineWidth: 1.5,
  914. states: {
  915. hover: {
  916. lineWidthPlus: 0,
  917. halo: false, // disable translucent halo on marker hover
  918. }
  919. },
  920. yAxis: 1,
  921. color: volumeColor,
  922. marker: {
  923. states: {
  924. hover: {
  925. lineWidth: 0,
  926. }
  927. },
  928. },
  929. tooltip: {
  930. pointFormatter: function() {
  931. return `<span style="color:${this.color}">\u25CF</span>`
  932. + ` ${this.series.name}:`
  933. + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}</b><br/>`;
  934. },
  935. },
  936. zIndex: 0,
  937. },
  938. ],
  939. yAxis: [
  940. {
  941. min: SettingsController.getStartChartAtZero() ? 0 : null,
  942. labels: {
  943. formatter: function() {
  944. return this.value.toLocaleString() + 'g';
  945. },
  946. x: -8,
  947. },
  948. showLastLabel: true, // show label at top of chart
  949. opposite: false,
  950. alignTicks: false
  951. }, {
  952. top: '75%',
  953. height: '25%',
  954. offset: 0,
  955. min: 0,
  956. opposite: false,
  957. tickPixelInterval: 35,
  958. allowDecimals: false,
  959. alignTicks: false,
  960. gridLineWidth: 0,
  961. labels: {
  962. enabled: SettingsController.getEnableFloatingVolumeLabels(),
  963. align: 'left',
  964. x: 0,
  965. style: {
  966. color: volumeLabelColor,
  967. },
  968. },
  969. showLastLabel: true,
  970. showFirstLabel: false,
  971. }],
  972. xAxis: {
  973. type: 'datetime',
  974. ordinal: false, // show continuous x axis if dates are missing
  975. plotBands: eventData,
  976. crosshair: {
  977. dashStyle: 'ShortDot',
  978. color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
  979. },
  980. dateTimeLabelFormats:{
  981. day: '%b %e',
  982. week: '%b %e, \'%y',
  983. month: '%b %Y',
  984. year: '%Y'
  985. },
  986. tickPixelInterval: 120,
  987. }
  988. });
  989.  
  990. containerElement.dataset.lastRendered = Date.now().toString();
  991. }
  992.  
  993. $.getJSON(`https://${markethuntApiDomain}/items/${itemId}/stock?&plugin_ver=${GM_info.script.version}`, function (response) {
  994. renderStockChart(response);
  995. });
  996. }
  997.  
  998. if (localStorage.markethuntEventDatesV2LastRetrieval === undefined) {
  999. updateEventData();
  1000. }
  1001.  
  1002. /*******************************
  1003. *
  1004. * Marketplace view observer
  1005. *
  1006. *******************************/
  1007.  
  1008. // chart-icon-90px.png minified with TinyPNG then converted to base 64
  1009. const chartIconImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAMAAAAPdrEwAAAAVFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
  1010. "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU4H24AAAAG3RSTlMABvTkbYjGz8kamvoO0EYg6H0koqabYVSqNDILUie0AAABU0lEQVRYw+2V2Y6DMAx" +
  1011. "FyUIgwLB1mxn//3+OhEQd6o5EypUqVT6PoBwcbmIXiqIo78b42traG7w5ftFCFeE1L+bFja7b0x0PVtesDmC1ZbWFpS/V2PQnYgIyfXOmBA9MP3KG/HlI+r9uY46vpj+It7f1tQvWhryL3lNC2" +
  1012. "wz/BFhn3/CuoS3taU4CPK2Pv7tcc++IYbkIsDxaMv+W+RpGG9wawQ1Q8lPcT27JF147BTuG69y0qZEDzOsYYeKSm3tEwxP52WR2DMb1BSPlU3bHECULeX6f86JkwWBfU9ep+dIhZ4olpsdOwj2" +
  1013. "bNdVjCzWlowVXmmMDNFYPLbTkVeXBsW/8toW6JPli12Z3QwnFns2i1bxZvJqR6cPUMn2UWqY/gtWUoGqxTJwZlFqeGZhanhmwmhI+Xi3SR6hl+jC1TB+spgRVq1rVqla1qlUNUSuKooD4Az6O4" +
  1014. "MtRLQLhAAAAAElFTkSuQmCC";
  1015.  
  1016. // sb.png minified with TinyPNG then converted to base 64
  1017. const sbImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAABcVBMVEX+/v/5///+/fzwyJTuxJD1////6cn416z31ab206SOi4ny///t///b" +
  1018. "///J///2/P/9/Pj4+Pjq6/b8+PPw8PPh4+7+9+3K5s//7M7/6Mb+5cP63bfv17b53LP21an1zJzWuprzzJDRrovpuoHmuXzktXu6k3Rd0HOzi2vp///Q//+z//+t///n/v/0+P/q7v/t7v79" +
  1019. "+PjW//P/+/P2+vHZ7PHn6e3/+eft5+f/9ubi8eT/9OPw59//8t377dzh3tz+7dXN7dTa09Sv1tP35tHO1czX4cu+3cLazcLCysL44L3Uw7vDuLj32rar3LGvtbHq167gyK6S16nlyanl06je" +
  1020. "w6jy06fxz6Z8oqSooKDszJ7Ls56MypvoxZqel5fQsZblwJWVkJB0xY3nvI10y4vswIv30IrvwInRp4jxyYeIg4XdtYPQq4LNpH65mnt9e3u+sXr0xHjZq3dZwHTPonR1dXHgqnC6kGtpbGnh" +
  1021. "qWEs0UvWjFe8AAAA4klEQVQY02PACvgYITSvlbo4mCEY4V9awZUf4+ieUqUOFmFK5OKKjMtKCioW9zPRBAowAhFIJUSnFhBrczMwAJGIkKiomQhIkFWHj0GXQc+An4df3yfPlRUoxMNgaGFv" +
  1022. "6uTpHF1SpqIA0StWWaCqzBwlL8+RngFxhnlhSJiblxSbhCRzEViE1ShNWlaGnZMzIFU1HqLLWFGOnZOZmYWFRcUD6g1FFg52DrnY3HINIahIpnJ2jpqGmlJCsjdUJFBJIViGTZJNOjwUKiLr" +
  1023. "KyXhYGtpbediAxURExYWYGIAQgGgDwEEwCDFO/6WiQAAAABJRU5ErkJggg==";
  1024.  
  1025. const settingsImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR4nO2YMUucQRCGH9Cgoi" +
  1026. "YgNolVqhAVm6SyNoGAyP0Q8TT+ABG0C3ZRbMU2gprC4vA/HNx5tQZTxSimCDHKJwsjHEe8b2bX1cV8DwzICTPvu+x+M7tQUFBwl3wFspzYI2G+Kwz8AjpJkCGF+Jt4Q4JMGQzMkiCLBgNfSIhhYAP4" +
  1027. "bTBwBWwCr+5D4AegDiyI2Bu6gU/ApUF4a1wCK0BPy4IsSE1XO4gnQKOlaA1YAg4ChLdGQ3LW/vG70+DN3B2K9I1ZX/EDwEkCBk6BQR8DawmIzyRWreLdQfqbgPCs6bCPWQxUAorVZN+OAL0SI3Ke6g" +
  1028. "F5K1rxJc8Cf4BpoKNNbve/GeDCs0ZJY+A1cOghfkK7QsA7DxOHok3FC6BqSO5W3krZkL8qmkw8A/aVe77dtrkNN1JrmuG+aPGiCziKOFl+zMl9JBqCOMsp0jwfWRlVNLFgrnKK9AXk7ld8/6MbcCJ8" +
  1029. "eXofBk5zirgmlewW6lIcYtdhfZmPeYi1n9F6wGe0Eeszam1kbjyIedeoWhqZzyhxIeOBlvcxR4mSMXGziXLOo1WnrHzUYS50nK5Lhx2VHtEnf88H3qMr/E8XGuQalyUSn/G81P9IQPxP30s9Mmk+tI" +
  1030. "EykR62NE1IG9+A5RgPW+2eFnvkWTDkaTEDtoHnsZ4WNbwE1o1G3IS7C4yTEFsGAzskyJzBgNsiyfHWYGCSBOkAzpUGmg9tUuwpxB8/tMiCgsfENevgYdmM/xZUAAAAAElFTkSuQmCC";
  1031.  
  1032. const kofiImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABm1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
  1033. "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AAAD/Xluzs7PIyMj/XV7/Wmz/W2b/XW" +
  1034. "D//f3/WXH/Wm7/W2n/XGT8/Pz/+Pn/XGLQ0ND/W2q6urq3t7esrKympqb/V3j/WHX/+vr19fX/ydDCwsKwsLCioqKMjIx9fX1YWFhKSko7Ozv9/f3//Pz5+fn/9fXm5ub/2tzb29v/0dXNzc3Ex" +
  1035. "MT/pqf/mqX/kKX/n56enp6RkZH/kpD/joz/i4mIiIj/h4b/hIJubm5iYmJNTU0jIyMeHh4PDw8MDAz/4+TS0tL/xcj/u8jFxcW+vr7/rbT/lqz/k6D/jZ3/gZz/i5r/ipmYmJiXl5f/mJaVlZX/" +
  1036. "lJWUlJT/kJP/eYz/comCgoJ5eXl0dHRra2s0NDQsLCwnJycTExMRERH4jQN3AAAAKXRSTlMAiqaR9J+W+/h6dQ+5rI6Jg4BvZ1ZJNycH1svGwmM9MyshCe/ksqE/Gb0RW4gAAAH8SURBVEjH7dR" +
  1037. "nVxpBGIbhiVnpKki1JWrq+4KKi7pgaAqIIGrsGks09vTee//ZbmFndhTQ73p9fu4zs+fsLrlwnvhdl2posnnthNN5DU8h1PuNQSNuh2oY+7jzDy1WwtTjItSWGT/Ahm5D0AOnmfuDJhrU1Qgy28" +
  1038. "XirzEJYGYfvWcJelBRmgFIYOvxQJx/8XrlSRTE7Kuvb7JiOXD4PO1YmobJ3+ZOGiyo+5V0Oj0y8m32bWSgr+/OsqgFdYQE2jEP8B3dNBgGWVZeRyKRAWU9OBgOP6YBuYITACE08cGyYT00FOv/w" +
  1039. "gK78BdgGBv44KW2Dqvr/t7eLRZ0C3s0YA/93LgOBoP3jl0pgSY+eCjfJKatVbM0sLdiCqCAbj6ApZg81tbqAVrQ1uG8jrsSTO9ZuojmZjkY/aSuNfdHQbGAMvNOBmANb7B36SloxQbdP4iCSlor" +
  1040. "bK7OAUDu0OJnQQ400Q19fxc4k6lDs5voTPgM9GJd3W/xeym3i+ZmYgjioJv6oNw/yrbvN38U9xFbbnFfXAgocSm4zvawiDLB4QkQgyZMAiPOTwETQofHZyc8F+ahmnGkd2c68CdUIZXw6sngtnD" +
  1041. "wqOoBbaQCG/4vJOLxeKL8X0kmk6lUfvXd5wkU6AEcqwUra/GRyrqaL+salb+j0+myWm02b4BcOK+OAN6AtFxkFAmAAAAAAElFTkSuQmCC";
  1042.  
  1043. const mpObserverTarget = document.querySelector("#overlayPopup");
  1044.  
  1045. function CurrentViewedItemId() {
  1046. const itemIdRaw = mpObserverTarget.querySelector(".marketplaceView-item.view")?.dataset.itemId;
  1047. return itemIdRaw ? Number(itemIdRaw) : null;
  1048. }
  1049.  
  1050. const mpObserver = new MutationObserver(function () {
  1051. // Check if the Marketplace interface is open
  1052. if (!mpObserverTarget.querySelector(".marketplaceView")) {
  1053. return;
  1054. }
  1055.  
  1056. // detect item page and inject chart
  1057. const backButton = mpObserverTarget.querySelector("a.marketplaceView-breadcrumb");
  1058. if (backButton) {
  1059. const targetContainer = mpObserverTarget.querySelector(".marketplaceView-item-description");
  1060.  
  1061. if (targetContainer && !mpObserverTarget.querySelector("#chartArea")) {
  1062. // Disconnect and reconnect later to prevent mutation loop
  1063. mpObserver.disconnect();
  1064.  
  1065. // Setup chart divs
  1066. const itemId = CurrentViewedItemId();
  1067.  
  1068. targetContainer.insertAdjacentHTML(
  1069. "beforebegin",
  1070. `<div id="chartArea" style="display: flex; padding: 0 20px 0 20px; height: 315px;">
  1071. <div style="flex-grow: 1; position: relative">
  1072. <div id="chartContainer" style="text-align: center; height: 100%; width: 100%">
  1073. <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
  1074. <div style="color: grey">Loading ...</div>
  1075. </div>
  1076. <div id="stockChartContainer" style="text-align: center; position: absolute; background-color: ${isDarkMode() ? darkMode.backgroundColor : 'white'}; top: 0; left: 0; width: 100%; height: 100%; display: none">
  1077. <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
  1078. <div style="color: grey">Loading ...</div>
  1079. </div>
  1080. </div>
  1081. <div id="markethuntInfobox" style="text-align: center; display: flex; flex-direction: column; padding: 34px 0 12px 5px; position: relative;">
  1082. <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans infobox-striped">
  1083. Trade volume:<br>
  1084. <span id="infoboxTradevol">--</span><br>
  1085. <span id="infoboxGoldvol" class="marketplaceView-goldValue">--</span>
  1086. </div>
  1087. <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans">
  1088. 7-day trade volume:<br>
  1089. <span id="infobox7dTradevol">--</span><br>
  1090. <span id="infobox7dGoldvol" class="marketplaceView-goldValue">--</span>
  1091. </div>
  1092. <div style="user-select: none; text-align: left">
  1093. <label class="cl-switch" for="markethuntShowStockData" style="cursor: pointer">
  1094. <input type="checkbox" id="markethuntShowStockData">
  1095. <span class="switcher"></span>
  1096. <span class="label">Stock chart</span>
  1097. </label>
  1098. </div>
  1099. <div style="flex-grow: 1"></div> <!-- spacer div -->
  1100. <div>
  1101. <div style="display: flex;">
  1102. <div style="flex-grow: 1;"></div>
  1103. <div>
  1104. <a id="markethuntSettingsLink" href="#">
  1105. <img src="${settingsImageData}" class="markethunt-settings-btn-img ${isDarkMode() ? 'inverted' : ''}">
  1106. </a>
  1107. </div>
  1108. <div>
  1109. <a href="https://ko-fi.com/vsong_program" target="_blank" alt="Donation Link">
  1110. <img src="${kofiImageData}" class="markethunt-settings-btn-img" alt="Settings">
  1111. </a>
  1112. </div>
  1113. <div style="flex-grow: 1;"></div>
  1114. </div>
  1115. <div style="font-size: 0.8em; color: grey">v${GM_info.script.version}</div>
  1116. </div>
  1117. </div>
  1118. </div>`
  1119. );
  1120.  
  1121. const itemPriceContainer = mpObserverTarget.querySelector(".marketplaceView-item-averagePrice");
  1122. itemPriceContainer.classList.add("infobox-stat");
  1123. itemPriceContainer.insertAdjacentHTML(
  1124. "beforeend",
  1125. `<br><span id="infoboxSbPrice" class="marketplaceView-sbValue">--</span><img style="vertical-align: bottom" src="${sbImageData}" alt="SB icon" />`
  1126. );
  1127.  
  1128. const itemPriceDisplay = itemPriceContainer.querySelector("span");
  1129. itemPriceDisplay.id = "infoboxPrice";
  1130.  
  1131. const infoBox = document.getElementById("markethuntInfobox");
  1132. infoBox.prepend(itemPriceContainer);
  1133.  
  1134. // Set infobox minimum width to prevent layout shifts, *then* reset price display
  1135. const infoBoxInitialWidth = $(infoBox).width();
  1136. infoBox.style.minWidth = `${infoBoxInitialWidth}px`;
  1137.  
  1138. itemPriceDisplay.innerHTML = "--";
  1139.  
  1140. // Set stock chart checkbox listener
  1141. const stockChartCheckbox = document.getElementById('markethuntShowStockData');
  1142. stockChartCheckbox?.addEventListener('change', (e) => {
  1143. if (e.target.checked) {
  1144. document.getElementById('stockChartContainer').style.display = 'block';
  1145. renderStockChartWithItemId(itemId, 'stockChartContainer');
  1146. } else {
  1147. document.getElementById('stockChartContainer').style.display = 'none';
  1148. renderChartWithItemId(itemId, 'chartContainer');
  1149. }
  1150. });
  1151.  
  1152. // Set Plugin Settings listener
  1153. const settingsLink = document.getElementById("markethuntSettingsLink");
  1154. settingsLink.addEventListener('click', openPluginSettings);
  1155.  
  1156. // Render chart
  1157. renderChartWithItemId(itemId, "chartContainer");
  1158.  
  1159. // Re-observe after mutation-inducing logic
  1160. mpObserver.observe(mpObserverTarget, {
  1161. childList: true,
  1162. subtree: true
  1163. });
  1164. }
  1165. }
  1166.  
  1167. // detect history page and inject portfolio buttons
  1168. const historyTab = mpObserverTarget.querySelector("[data-tab=history].active");
  1169. if (SettingsController.getEnablePortfolioButtons() && historyTab) {
  1170. mpObserver.disconnect();
  1171.  
  1172. let rowElem = mpObserverTarget.querySelectorAll(".marketplaceMyListings tr.buy");
  1173. rowElem.forEach(function(row) {
  1174. if (!row.querySelector(".mousehuntActionButton.tiny.addPortfolio")) {
  1175. let itemElem = row.querySelector(".marketplaceView-itemImage");
  1176. const itemId = itemElem.getAttribute("data-item-id");
  1177.  
  1178. let qtyElem = row.querySelector("td.marketplaceView-table-numeric");
  1179. const qty = Number(qtyElem.innerText.replace(/\D/g, ''));
  1180.  
  1181. let priceElem = row.querySelector("td.marketplaceView-table-numeric .marketplaceView-goldValue");
  1182. const price = Number(priceElem.innerText.replace(/\D/g, ''));
  1183.  
  1184. let buttonContainer = row.querySelector("td.marketplaceView-table-actions");
  1185. let addPortfolioBtn = document.createElement("a");
  1186. addPortfolioBtn.href = `https://${markethuntDomain}/portfolio.php?action=add_position&item_id=${itemId}&add_qty=${qty}&add_mark=${price}`;
  1187. addPortfolioBtn.innerHTML = "<span>+ Portfolio</span>";
  1188. addPortfolioBtn.className = "mousehuntActionButton tiny addPortfolio lightBlue";
  1189. addPortfolioBtn.target = "_blank";
  1190. addPortfolioBtn.style.display = "block";
  1191. addPortfolioBtn.style.marginTop = "2px";
  1192. buttonContainer.appendChild(addPortfolioBtn);
  1193. }
  1194. });
  1195.  
  1196. mpObserver.observe(mpObserverTarget, {
  1197. childList: true,
  1198. subtree: true
  1199. });
  1200. }
  1201. });
  1202.  
  1203. // Initial observe
  1204. mpObserver.observe(mpObserverTarget, {
  1205. childList: true,
  1206. subtree: true
  1207. });
  1208.  
  1209. const marketplaceCssOverrides = `
  1210. .marketplaceView-item {
  1211. padding-top: 10px;
  1212. }
  1213. .marketplaceView-item-content {
  1214. padding-top: 10px;
  1215. padding-bottom: 0px;
  1216. min-height: 0px;
  1217. }
  1218. .marketplaceView-item-descriptionContainer {
  1219. padding-bottom: 5px;
  1220. padding-top: 5px;
  1221. }
  1222. .marketplaceView-item-averagePrice {
  1223. margin-top: 5px;
  1224. }
  1225. .marketplaceView-item-footer {
  1226. padding-top: 10px;
  1227. padding-bottom: 10px;
  1228. }
  1229. .markethunt-cross-link {
  1230. color: #000;
  1231. font-size: 10px;
  1232. background: #fff;
  1233. display: block;
  1234. padding: 0.1em 0;
  1235. margin: 0.4em 0;
  1236. box-shadow: #797979 1px 1px 1px 0;
  1237. border-radius: 0.2em;
  1238. border: 1px solid #a8a8a8;
  1239. }
  1240. .markethunt-cross-link:hover {
  1241. color: white;
  1242. background-color: ${primaryLineColor};
  1243. }
  1244. .markethunt-settings-btn-img {
  1245. height: 26px;
  1246. padding: 3px;
  1247. margin: 0 5px 0 5px;
  1248. border-radius: 999px;
  1249. box-shadow: 0px 0px 3px gray;
  1250. }
  1251. .markethunt-settings-btn-img:hover {
  1252. box-shadow: 0px 0px 3px 1px gray;
  1253. }
  1254. .inverted {
  1255. filter: invert(1);
  1256. }
  1257. .markethunt-settings-row-input {
  1258. display: flex;
  1259. align-items: center;
  1260. padding-right: 5px;
  1261. }
  1262. .markethunt-settings-row {
  1263. display: flex;
  1264. padding: 5px;
  1265. }
  1266. .markethunt-settings-row-description {
  1267. }
  1268. .marketplaceView-item-averagePrice.infobox-stat {
  1269. text-align: left;
  1270. margin-bottom: 14px;
  1271. white-space: nowrap;
  1272. }
  1273. .marketplaceView-item-leftBlock .marketplaceHome-block-viewAll {
  1274. margin-top: 5px;
  1275. }
  1276. .infobox-striped {
  1277. }
  1278. .infobox-small-spans span {
  1279. font-size: 11px;
  1280. }
  1281. .infobox-small-spans .marketplaceView-goldValue::after {
  1282. width: 17px;
  1283. height: 13px;
  1284. }
  1285. `;
  1286.  
  1287. const materialSwitchCss = `
  1288. .cl-switch input[type="checkbox"] {
  1289. display: none;
  1290. visibility: hidden;
  1291. }
  1292.  
  1293. .cl-switch .switcher {
  1294. display: inline-block;
  1295. border-radius: 100px;
  1296. width: 2.25em;
  1297. height: 1em;
  1298. background-color: #ccc;
  1299. position: relative;
  1300. -webkit-box-sizing: border-box;
  1301. -moz-box-sizing: border-box;
  1302. box-sizing: border-box;
  1303. vertical-align: middle;
  1304. cursor: pointer;
  1305. }
  1306.  
  1307. .cl-switch .switcher:before {
  1308. content: "";
  1309. display: block;
  1310. width: 1.3em;
  1311. height: 1.3em;
  1312. background-color: #fff;
  1313. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  1314. border-radius: 50%;
  1315. margin-top: -0.15em;
  1316. position: absolute;
  1317. top: 0;
  1318. left: 0;
  1319. -webkit-box-sizing: border-box;
  1320. -moz-box-sizing: border-box;
  1321. box-sizing: border-box;
  1322. margin-right: 0;
  1323. -webkit-transition: all 0.2s;
  1324. -moz-transition: all 0.2s;
  1325. -ms-transition: all 0.2s;
  1326. -o-transition: all 0.2s;
  1327. transition: all 0.2s;
  1328. }
  1329.  
  1330. .cl-switch .switcher:active:before {
  1331. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 0 0 1em rgba(63, 81, 181, 0.3);
  1332. transition: all, 0.1s;
  1333. }
  1334.  
  1335. .cl-switch .label {
  1336. cursor: pointer;
  1337. vertical-align: middle;
  1338. }
  1339.  
  1340. .cl-switch input[type="checkbox"]:checked+.switcher {
  1341. background-color: #8591d5;
  1342. }
  1343.  
  1344. .cl-switch input[type="checkbox"]:checked+.switcher:before {
  1345. left: 100%;
  1346. margin-left: -1.3em;
  1347. background-color: #3f51b5;
  1348. }
  1349.  
  1350. .cl-switch [disabled]:not([disabled="false"])+.switcher {
  1351. background: #ccc !important;
  1352. }
  1353.  
  1354. .cl-switch [disabled]:not([disabled="false"])+.switcher:active:before {
  1355. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
  1356. }
  1357.  
  1358. .cl-switch [disabled]:not([disabled="false"])+.switcher:before {
  1359. background-color: #e2e2e2 !important;
  1360. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
  1361. }`;
  1362.  
  1363. /*******************************
  1364. *
  1365. * Journal observer
  1366. *
  1367. *******************************/
  1368.  
  1369. // add_portfolio_journal.png minified with TinyPNG then converted to base 64
  1370. const addPfolioBtnImgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALBAMAAACEzBAKAAAAFVBMVEUAAAAAAAD/swD06DD6zhj///8PDgMsru0CAAAAAXRSTlMA" +
  1371. "QObYZgAAADhJREFUCNdjCBQEAwEGYWMwMERmmBkbCoIZhkAobMgg4igo6Cjo4sggpKQIhEqKyAwgQGEwQk0GAIl6DBhSGEjXAAAAAElFTkSuQmCC";
  1372.  
  1373. function addJournalButtons(supplyTransferJournalEntries) {
  1374. supplyTransferJournalEntries.forEach(function(supplyTransferEntry) {
  1375. const journalActionsElem = supplyTransferEntry.querySelector(".journalactions");
  1376. const textElem = supplyTransferEntry.querySelector(".journaltext");
  1377.  
  1378. if (journalActionsElem.querySelector("a.actionportfolio")) {
  1379. return;
  1380. }
  1381. if (textElem.textContent.includes("SUPER|brie+") || textElem.textContent.includes("Passing Parcel")) {
  1382. return;
  1383. }
  1384. // Disable button on sending transfers until portfolio sending feature implemented
  1385. if (textElem.textContent.includes("I sent")) {
  1386. return;
  1387. }
  1388.  
  1389. const addPortfolioBtn = document.createElement("a");
  1390. addPortfolioBtn.href = "#";
  1391. addPortfolioBtn.className = "actionportfolio";
  1392. addPortfolioBtn.addEventListener('click', addSbTradeToPortfolio);
  1393. journalActionsElem.prepend(addPortfolioBtn)
  1394. });
  1395. }
  1396.  
  1397. async function updateItemMetadata() {
  1398. console.log("Retrieving marketplace item data");
  1399. return new Promise((resolve, reject) => {
  1400. hg.utils.Marketplace.getMarketplaceData(
  1401. function (response) {
  1402. const itemMetadata = response.marketplace_items.reduce(
  1403. function (items, item) {
  1404. items[normalizeItemName(item.name)] = item.item_id;
  1405. return items;
  1406. },
  1407. {}
  1408. );
  1409. localStorage.markethuntItemMetadata = JSON.stringify(itemMetadata);
  1410. localStorage.markethuntItemMetadataLastRetrieval = Date.now();
  1411. resolve(itemMetadata);
  1412. },
  1413. function (e) {
  1414. reject(e);
  1415. }
  1416. );
  1417. });
  1418. }
  1419.  
  1420. function normalizeItemName(name) {
  1421. return name.trim();
  1422. }
  1423.  
  1424. async function addSbTradeToPortfolio(event) {
  1425. event.preventDefault(); // prevent scroll to top
  1426.  
  1427. const targetTransferJournalEntry = event.target.parentNode.parentNode.parentNode;
  1428. const textElem = targetTransferJournalEntry.querySelector(".journaltext");
  1429. const targetEntryId = Number(targetTransferJournalEntry.dataset.entryId);
  1430. // group 1 = qty, group 2 = item name, group 3 = trade partner snuid
  1431. const regex = /^I received (\d[\d,]*) (.+?) from <a href.+snuid=(\w+)/
  1432.  
  1433. // get item and partner data
  1434. const targetEntryMatch = textElem.innerHTML.match(regex);
  1435. const targetItemQty = Number(targetEntryMatch[1].replace(",", ""));
  1436. const targetItemName = targetEntryMatch[2];
  1437. const partnerSnuid = targetEntryMatch[3];
  1438. const partnerName = textElem.querySelector('a').innerHTML;
  1439.  
  1440. // get item ID
  1441. let targetItemId = undefined;
  1442. if (localStorage.markethuntItemMetadata !== undefined) {
  1443. const itemMetadata = JSON.parse(localStorage.markethuntItemMetadata);
  1444. targetItemId = itemMetadata[normalizeItemName(targetItemName)];
  1445. }
  1446.  
  1447. if (targetItemId === undefined) {
  1448. $.toast({
  1449. text: "Please wait ...",
  1450. heading: localStorage.markethuntItemMetadata === undefined ? 'Downloading item data' : 'Reloading item data',
  1451. icon: 'info',
  1452. position: 'top-left',
  1453. loader: false, // Whether to show loader or not. True by default
  1454. });
  1455. const itemMetadata = await updateItemMetadata();
  1456. await sleep(600); // allow user to read toast before opening new tab
  1457. targetItemId = itemMetadata[normalizeItemName(targetItemName)];
  1458. }
  1459.  
  1460. // detect all sb send entries
  1461. const allSupplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
  1462. const matchingSbSendEntries = Array.from(allSupplyTransferJournalEntries).reduce(
  1463. function(results, journalEntry) {
  1464. const innerHTML = journalEntry.querySelector(".journaltext").innerHTML;
  1465. if (!innerHTML.includes(partnerSnuid)) {
  1466. return results;
  1467. }
  1468. const candidateSbMatch = innerHTML.match(/^I sent (\d[\d,]*) SUPER\|brie\+ to <a href/);
  1469. if (!candidateSbMatch) {
  1470. return results;
  1471. }
  1472. const candidateSbSent = Number(candidateSbMatch[1].replace(",", ""));
  1473. const candidateEntryId = Number(journalEntry.dataset.entryId);
  1474. results.push({sbSent: candidateSbSent, entryId: candidateEntryId});
  1475. return results;
  1476. },
  1477. []
  1478. );
  1479.  
  1480. // choose best sb send entry
  1481. let bestSbSendEntryMatch = null;
  1482. let bestMatchDistance = null;
  1483. matchingSbSendEntries.forEach(function(candidateEntry) {
  1484. const entryPairDistance = Math.abs(targetEntryId - candidateEntry.entryId);
  1485. if (bestMatchDistance === null || bestMatchDistance > entryPairDistance) {
  1486. bestSbSendEntryMatch = candidateEntry;
  1487. bestMatchDistance = entryPairDistance;
  1488. }
  1489. });
  1490.  
  1491. let avgSbPriceString = "none";
  1492. if (bestSbSendEntryMatch !== null) {
  1493. const avgSbPrice = bestSbSendEntryMatch.sbSent / targetItemQty;
  1494. avgSbPriceString = avgSbPrice.toFixed(2);
  1495. }
  1496.  
  1497. // prepare modal message
  1498. let actionMsg = 'Markethunt plugin: ';
  1499. if (bestSbSendEntryMatch !== null) {
  1500. actionMsg += `Found a transfer of ${bestSbSendEntryMatch.sbSent.toLocaleString()} SB to ${partnerName}.` +
  1501. ` Buy price has been filled in for you.`;
  1502. } else {
  1503. actionMsg += 'No matching SB transfer found. Please fill in buy price manually.';
  1504. }
  1505.  
  1506. // open in new tab
  1507. window.open(`https://${markethuntDomain}/portfolio.php?action=add_position` +
  1508. `&action_msg=${encodeURIComponent(actionMsg)}` +
  1509. `&item_id=${targetItemId}` +
  1510. `&add_qty=${targetItemQty}` +
  1511. `&add_mark=${avgSbPriceString}` +
  1512. `&add_mark_type=sb`,
  1513. '_blank');
  1514. }
  1515.  
  1516. const journalObserverTarget = document.querySelector("#mousehuntContainer");
  1517. const journalObserver = new MutationObserver(function () {
  1518. // Disconnect and reconnect later to prevent mutation loop
  1519. journalObserver.disconnect();
  1520.  
  1521. const journalContainer = journalObserverTarget.querySelector("#journalContainer");
  1522. if (SettingsController.getEnablePortfolioButtons() && journalContainer) {
  1523. // add portfolio buttons
  1524. const supplyTransferJournalEntries = journalContainer.querySelectorAll("div.entry.supplytransferitem");
  1525. addJournalButtons(supplyTransferJournalEntries);
  1526. }
  1527.  
  1528. // Reconnect observer once all mutations done
  1529. journalObserver.observe(journalObserverTarget, {
  1530. childList: true,
  1531. subtree: true
  1532. });
  1533. });
  1534.  
  1535. // Initial observe
  1536. journalObserver.observe(journalObserverTarget, {
  1537. childList: true,
  1538. subtree: true
  1539. });
  1540.  
  1541. const journalCssOverrides = `
  1542. .journalactions a {
  1543. display: inline-block;
  1544. }
  1545. .journalactions a.actionportfolio {
  1546. margin-right: 5px;
  1547. background: url('${addPfolioBtnImgData}');
  1548. width: 16px;
  1549. }
  1550. `;
  1551.  
  1552. /*******************************
  1553. *
  1554. * Import Portfolio
  1555. *
  1556. *******************************/
  1557.  
  1558. function addTouchPoint() {
  1559. if ($('.invImport').length === 0) {
  1560. const invPages = $('.inventory .torn_pages');
  1561. //Inventory History Button
  1562. const invImportElem = document.createElement('li');
  1563. invImportElem.classList.add('crafting');
  1564. invImportElem.classList.add('invImport');
  1565. const invImportBtn = document.createElement('a');
  1566. invImportBtn.href = "#";
  1567. invImportBtn.innerText = "Export to Markethunt";
  1568. invImportBtn.onclick = function () {
  1569. onInvImportClick();
  1570. };
  1571. const icon = document.createElement("div");
  1572. icon.className = "icon";
  1573. invImportBtn.appendChild(icon);
  1574. invImportElem.appendChild(invImportBtn);
  1575. $(invImportElem).insertAfter(invPages);
  1576. }
  1577. }
  1578.  
  1579. function submitInv() {
  1580. if (!document.forms["import-form"].reportValidity()) {
  1581. return;
  1582. }
  1583.  
  1584. const gold = Number($('.hud_gold').text().replaceAll(/[^\d]/g, ''));
  1585.  
  1586. if (isNaN(gold)) {
  1587. return;
  1588. }
  1589.  
  1590. const itemsToGet = ['weapon','base', 'trinket', 'bait', 'skin', 'crafting_item','convertible', 'potion', 'stat','collectible','map_piece','adventure']; //future proof this to allow for exclusions
  1591. hg.utils.UserInventory.getItemsByClass(itemsToGet, true, function(data) {
  1592. let importData = {
  1593. itemsArray: [],
  1594. inventoryGold: gold
  1595. };
  1596. data.forEach(function(arrayItem, index) {
  1597. importData.itemsArray[index] = [arrayItem.item_id, arrayItem.quantity];
  1598. });
  1599.  
  1600. $('#import-data').val(JSON.stringify(importData));
  1601. document.forms["import-form"].submit();
  1602. })
  1603. }
  1604.  
  1605. function onInvImportClick(){
  1606. $.dialog({
  1607. title: 'Export inventory to Markethunt',
  1608. content: `
  1609. <form id="import-form" name="import-form" action="https://${markethuntDomain}/import_portfolio.php" method="post" target="_blank">
  1610. <label for="import-portfolio-name">Portfolio name: <span style="color: red">*</span></label>
  1611. <input type="text" id="import-portfolio-name" name="import-portfolio-name" required pattern=".+"/>
  1612. <input type="hidden" id="import-data" name="import-data"/>
  1613. </form>
  1614. <div id="export-dialog-buttons" class="jconfirm-buttons" style="float: none; margin-top: 10px;"><button type="button" class="btn btn-primary">Export</button></div>`,
  1615. boxWidth: '600px',
  1616. useBootstrap: false,
  1617. closeIcon: true,
  1618. draggable: true,
  1619. onOpen: function(){
  1620. $('#import-portfolio-name').val('Portfolio ' + (new Date()).toISOString().substring(0, 10));
  1621. this.$content.find('button').click(function(){
  1622. submitInv();
  1623. });
  1624. }
  1625. });
  1626. }
  1627.  
  1628. /*******************************
  1629. *
  1630. * Final setup and add css
  1631. *
  1632. *******************************/
  1633.  
  1634. $(document).ready(function() {
  1635. GM_addStyle(GM_getResourceText("jq_confirm_css"));
  1636. GM_addStyle(GM_getResourceText("jq_toast_css"));
  1637. GM_addStyle(marketplaceCssOverrides);
  1638. GM_addStyle(journalCssOverrides);
  1639. GM_addStyle(materialSwitchCss);
  1640.  
  1641. addTouchPoint();
  1642.  
  1643. const supplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
  1644. addJournalButtons(supplyTransferJournalEntries);
  1645. });