ebaytotalprice-userscript

Add the total eBay auction price including postage in the auction listing

当前为 2021-10-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ebaytotalprice-userscript
  3. // @namespace https://github.com/subz390
  4. // @version 2.3.1.211012101701
  5. // @description Add the total eBay auction price including postage in the auction listing
  6. // @author SubZ390
  7. // @license MIT
  8. // @run-at document-idle
  9. // @grant none
  10. // @noframes
  11. // @include /^https?://(ar|b[ory]|c[lor]|do|ec|gt|hn|il|kz|mx|ni|p[aerty]|ru|sv|uy|ve|www)\.ebay\.com/
  12. // @include /^https?://www\.ebay\.com\.au/
  13. // @include /^https?://www\.ebay\.co\.uk/
  14. // @include /^https?://www\.ebay\.(at|ca|de|es|fr|ie|it|nl|ph|pl)/
  15. // @include /^https?://www\.be(nl|fr)\.ebay\.be/
  16. //
  17. //
  18. // ==/UserScript==
  19.  
  20. function realTypeOf(object, lowerCase = true) {
  21. if (typeof object !== 'object') {return typeof object}
  22. if (object === null) {return 'null'}
  23. const internalClass = Object.prototype.toString.call(object).slice(8, -1);
  24. return lowerCase === true ? internalClass.toLowerCase() : internalClass
  25. }
  26.  
  27. function waitForMini({tryFor = 3, every = 100, test = () => false, success = () => null, timeout = () => null} = {}) {
  28. function leadingEdge() {
  29. const testResult = test();
  30. if (testResult) {
  31. success(testResult);
  32. return true
  33. }
  34. return false
  35. }
  36. if (leadingEdge() === false) {
  37. const intervalReference = setInterval(() => {
  38. const testResult = test();
  39. if (testResult) {
  40. clearInterval(intervalReference);
  41. clearTimeout(setTimeoutReference);
  42. success(testResult);
  43. }
  44. }, every);
  45. const setTimeoutReference = setTimeout(() => {
  46. clearInterval(intervalReference);
  47. timeout();
  48. }, tryFor * 1000);
  49. }
  50. }
  51.  
  52. function getNode(node = '', debug = undefined, scope = document) {
  53. try {
  54. scope = scope === null ? document : scope;
  55. const nodeType = realTypeOf(node);
  56. if (nodeType == 'string') {
  57. if (node == '') {return null}
  58. let scopeType = realTypeOf(scope);
  59. if (scopeType == 'text') {
  60. return null
  61. }
  62. if (scopeType == 'string') {
  63. const tempScope = document.querySelector(scope);
  64. if (tempScope == null) {
  65. return null
  66. }
  67. scope = tempScope;
  68. }
  69. scopeType = realTypeOf(scope);
  70. if (scopeType.search(/array|nodelist|svgsvgelement|html/i) !== -1) {
  71. nodeType;
  72. const element = scope.querySelector(node);
  73. return element
  74. }
  75. else {
  76. return null
  77. }
  78. }
  79. else if (nodeType.search(/array|nodelist|svgsvgelement|html/i) !== -1) {
  80. return node
  81. }
  82. else if (nodeType.search(/null/) !== -1) {
  83. return null
  84. }
  85. else {
  86. return null
  87. }
  88. }
  89. catch (error) {
  90. console.error(error);
  91. }
  92. }
  93.  
  94. function appendStyle({style: styleString, className = undefined, whereAdjacent = 'afterend', whereTarget = 'body', tryFor = 5, failMessage = undefined}) {
  95. return new Promise((resolve, reject) => {
  96. const styleElement = document.createElement('style');
  97. styleElement.appendChild(document.createTextNode(styleString));
  98. if (className) {styleElement.className = className;}
  99. function appendTarget(targetNode, styleElement) {
  100. if (whereAdjacent !== undefined) {
  101. return targetNode.insertAdjacentElement(whereAdjacent, styleElement)
  102. }
  103. else {
  104. return targetNode.appendChild(styleElement)
  105. }
  106. }
  107. waitForMini({
  108. tryFor: tryFor,
  109. every: 100,
  110. test: () => getNode(whereTarget),
  111. success: (targetNode) => {resolve(appendTarget(targetNode, styleElement));},
  112. timeout: () => reject(Error(failMessage || `appendStyle timed out whilst waiting for targetNode: ${whereTarget}`))
  113. });
  114. })
  115. }
  116.  
  117. function sprintf2({template = '', regex = /{([^{}]+)}/g, values} = {}) {
  118. if (template === '') {
  119. console.warn('template is an empty string');
  120. return null
  121. }
  122. function templateReplace(replaceTemplate, replaceValues) {
  123. return replaceTemplate.replace(regex, (match, name) => {
  124. if (replaceValues[name]) {
  125. if (typeof replaceValues[name] === 'function') {
  126. return replaceValues[name]().toString()
  127. }
  128. return replaceValues[name] || match
  129. }
  130. if (typeof replaceValues[name] === 'string' && replaceValues[name].length == 0) {
  131. return ''
  132. }
  133. return match
  134. })
  135. }
  136. if (Array.isArray(values)) {
  137. values.forEach((object) => {template = templateReplace(template, object);});
  138. return template
  139. }
  140. else {
  141. return templateReplace(template, values)
  142. }
  143. }
  144.  
  145. function findMatch(string, regex, index = 1) {
  146. if (string === null) return null
  147. const m = string.match(regex);
  148. return (m) ? (index=='all' ? m : (m[index] ? m[index] : m[0])) : null
  149. }
  150.  
  151. function qs({selector = null, scope = document, array = false, all = false, contains = null, unittest = false, debugTag = ''} = {}) {
  152. const language = {
  153. en: {
  154. selectorUndefined: `${debugTag}selector is undefined`,
  155. scopeNotUseable: `${debugTag}scope is not useable`,
  156. }
  157. };
  158. if (unittest === 'language') {return language}
  159. try {
  160. if (selector === null) {
  161. console.error(language.en.selectorUndefined);
  162. return null
  163. }
  164. if (scope !== document) {
  165. scope = getNode(scope);
  166. if (scope === null) {
  167. return null
  168. }
  169. }
  170. if (unittest === 'scope') {return scope}
  171. if (unittest === 'options') {
  172. return {
  173. selector: selector,
  174. scope: scope,
  175. array: array,
  176. all: all,
  177. contains: contains,
  178. unittest: unittest
  179. }
  180. }
  181. if (all === true) {
  182. const staticNodeList = scope.querySelectorAll(selector);
  183. if (staticNodeList.length === 0) {return null}
  184. if (array === true) {
  185. if (contains !== null) {
  186. const tempArray = [];
  187. staticNodeList.forEach((element) => {
  188. if (element.textContent.search(contains) !== -1) {
  189. tempArray.push(element);
  190. }
  191. });
  192. if (tempArray.length === 0) {return null}
  193. else {return tempArray}
  194. }
  195. return Array.from(staticNodeList)
  196. }
  197. else {
  198. if (contains !== null) {
  199. for (let index = 0; index < staticNodeList.length; index++) {
  200. if (staticNodeList[index].textContent.search(contains) !== -1) {
  201. return staticNodeList
  202. }
  203. }
  204. return null
  205. }
  206. return staticNodeList
  207. }
  208. }
  209. else {
  210. const qsHTMLElement = scope.querySelector(selector);
  211. if (qsHTMLElement === null) {return null}
  212. if (typeof contains === 'string' || contains instanceof RegExp) {
  213. if (qsHTMLElement.textContent.search(contains) === -1) {return null}
  214. }
  215. if (array === true) {return [qsHTMLElement]}
  216. else {return qsHTMLElement}
  217. }
  218. }
  219. catch (error) {
  220. console.error(error);
  221. }
  222. }
  223.  
  224. const globals = {
  225. priceMatchRegExp: /((\d+[,\.])+\d+)/,
  226. currencySymbolsRegExp: /(\$|EUR|PHP|zł|£)/,
  227. itemPriceElementTemplate: '<span class="total-price">{currencySymbol}{totalPrice}</span>',
  228. itemPriceElementTemplateSelector: 'span.total-price',
  229. itemPriceElementInnerTextTemplate: '{currencySymbol}{totalPrice}',
  230. };
  231.  
  232. function processMethod(options) {
  233. try {
  234. function getMethod() {
  235. for (const [, method] of Object.entries(options)) {
  236. for (let index = 0; index < method.identifierSelector.length; index++) {
  237. const selector = method.identifierSelector[index];
  238. const identifierNode = getNode(selector);
  239. if (identifierNode !== null) {return method}
  240. }
  241. }
  242. return null
  243. }
  244. const method = getMethod(options);
  245. if (method !== null) {method.process();}
  246. }
  247. catch (error) {console.error(error);}
  248. }
  249.  
  250. function getValue(element) {
  251. try {
  252. let value = findMatch(element.textContent.trim(), globals.priceMatchRegExp);
  253. value = value.replace(/[,\.]/g, '');
  254. value = parseFloat(value);
  255. return value
  256. }
  257. catch (error) {
  258. console.error(error);
  259. return null
  260. }
  261. }
  262.  
  263. function processItemListing({listItemsSelector, itemPriceElementSelector, convertPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, convertShippingElementSelector, itemShippingElementTemplate = null}) {
  264. const content = qs({selector: listItemsSelector});
  265. if (content) {
  266. const itemPriceElement = qs({selector: convertPriceElementSelector, scope: content, contains: /\d/}) || qs({selector: itemPriceElementSelector, scope: content, contains: /\d/});
  267. const itemShippingElement = qs({selector: convertShippingElementSelector, scope: content, contains: /\d/}) || qs({selector: itemShippingElementSelector, scope: content, contains: /\d/});
  268. if (itemPriceElement && itemShippingElement) {
  269. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  270. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  271. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  272. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  273. const HTML = sprintf2({
  274. template: itemShippingElementTemplate || itemPriceElementTemplate,
  275. values: {
  276. itemPrice: itemPriceElement.textContent.trim(),
  277. itemShippingAmount: itemShippingElement.textContent.trim(),
  278. currencySymbol: shippingCurrencySymbol,
  279. totalPrice: totalPrice
  280. }});
  281. if (itemPriceElementTemplate) {
  282. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  283. }
  284. else {
  285. itemShippingElement.innerHTML = HTML;
  286. }
  287. const itemPriceElementObserver = new MutationObserver((mutationList, observer) => {
  288. mutationList.forEach((mutation) => {
  289. mutation.addedNodes.forEach((element) => {
  290. if (element.nodeName == '#text') {
  291. const totalPriceElement = getNode(globals.itemPriceElementTemplateSelector);
  292. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  293. const totalPriceText = sprintf2({
  294. template: globals.itemPriceElementInnerTextTemplate,
  295. values: {
  296. itemPrice: itemPriceElement.textContent.trim(),
  297. itemShippingAmount: itemShippingElement.textContent.trim(),
  298. currencySymbol: shippingCurrencySymbol,
  299. totalPrice: totalPrice
  300. }});
  301. totalPriceElement.textContent = totalPriceText;
  302. }
  303. });
  304. });
  305. });
  306. itemPriceElementObserver.observe(itemPriceElement, {childList: true});
  307. }
  308. }
  309. }
  310. }
  311.  
  312. function processListGallery({listItemsSelector, itemPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, itemShippingElementTemplate = null}) {
  313. const listItems = qs({selector: listItemsSelector, all: true, array: true});
  314. if (listItems) {
  315. for (let i=0; listItems[i]; i++) {
  316. const itemPriceElement = qs({selector: itemPriceElementSelector, scope: listItems[i]});
  317. const itemShippingElement = qs({selector: itemShippingElementSelector, scope: listItems[i], contains: /\d/});
  318. if (itemPriceElement && itemShippingElement) {
  319. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  320. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  321. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  322. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  323. const HTML = sprintf2({
  324. template: itemShippingElementTemplate || itemPriceElementTemplate,
  325. values: {
  326. itemPrice: itemPriceElement.textContent.trim(),
  327. itemShippingAmount: itemShippingElement.textContent.trim(),
  328. currencySymbol: shippingCurrencySymbol,
  329. totalPrice: totalPrice
  330. }});
  331. if (itemPriceElementTemplate) {
  332. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  333. }
  334. else {
  335. itemShippingElement.innerHTML = HTML;
  336. }
  337. }
  338. }
  339. }
  340. }
  341. }
  342.  
  343. var stylesheet = ".total-price{background:#bf0;color:#f00!important;outline:2px solid;padding:1px 4px;margin-left:5px;font-size:20px!important;font-weight:400!important;}.s-item__detail{overflow:visible!important;}";
  344.  
  345. appendStyle({style: stylesheet});
  346. processMethod({
  347. search: {
  348. identifierSelector: ['#mainContent ul.srp-results', '#mainContent ul.b-list__items_nofooter'],
  349. process: () => processListGallery({
  350. listItemsSelector: '#mainContent li.s-item',
  351. itemPriceElementSelector: '.s-item__price',
  352. itemShippingElementSelector: '.s-item__shipping',
  353. itemPriceElementTemplate: globals.itemPriceElementTemplate
  354. })
  355. },
  356. sch: {
  357. identifierSelector: ['#mainContent ul#ListViewInner'],
  358. process: () => processListGallery({
  359. listItemsSelector: '#mainContent li',
  360. itemPriceElementSelector: '.lvprice span',
  361. itemShippingElementSelector: '.lvshipping span.fee',
  362. itemPriceElementTemplate: globals.itemPriceElementTemplate
  363. })
  364. },
  365. itm: {
  366. identifierSelector: ['#mainContent form[name="viactiondetails"]'],
  367. process: () => processItemListing({
  368. listItemsSelector: '#mainContent',
  369. itemPriceElementSelector: 'span[itemprop="price"]',
  370. convertPriceElementSelector: '#prcIsumConv',
  371. itemShippingElementSelector: '#fshippingCost',
  372. convertShippingElementSelector: '#convetedPriceId',
  373. itemPriceElementTemplate: globals.itemPriceElementTemplate
  374. })
  375. }
  376. });