ebaytotalprice-userscript

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

目前為 2021-02-09 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ebaytotalprice-userscript
  3. // @namespace https://github.com/subz390
  4. // @version 2.2.0.210209163142
  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).match(/\[object\s(\w+)\]/)[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. if (typeof node == 'string') {
  56. if (node == '') {return null}
  57. if (typeof scope == 'string') {
  58. const tempScope = document.querySelector(scope);
  59. if (tempScope == null) {
  60. return null
  61. }
  62. scope = tempScope;
  63. }
  64. const element = scope.querySelector(node);
  65. return element
  66. }
  67. return node
  68. }
  69. catch (error) {
  70. console.error(error);
  71. }
  72. }
  73.  
  74. function appendStyle({style: styleString, className = undefined, whereAdjacent = 'afterend', whereTarget = 'body', tryFor = 5, failMessage = undefined}) {
  75. return new Promise((resolve, reject) => {
  76. const styleElement = document.createElement('style');
  77. styleElement.appendChild(document.createTextNode(styleString));
  78. if (className) {styleElement.className = className;}
  79. function appendTarget(targetNode, styleElement) {
  80. if (whereAdjacent !== undefined) {
  81. return targetNode.insertAdjacentElement(whereAdjacent, styleElement)
  82. }
  83. else {
  84. return targetNode.appendChild(styleElement)
  85. }
  86. }
  87. waitForMini({
  88. tryFor: tryFor,
  89. every: 100,
  90. test: () => getNode(whereTarget),
  91. success: (targetNode) => {resolve(appendTarget(targetNode, styleElement));},
  92. timeout: () => reject(Error(failMessage || `appendStyle timed out whilst waiting for targetNode: ${whereTarget}`))
  93. });
  94. })
  95. }
  96.  
  97. function findMatch(string, regex, index) {
  98. if (string === null) return null
  99. index = index || 1;
  100. const m = string.match(regex);
  101. return (m) ? (index=='all' ? m : (m[index] ? m[index] : m[0])) : null
  102. }
  103.  
  104. function setDefault(paramOptions, paramDefault = undefined, paramAction = undefined, debug = undefined) {
  105. let globalObject;
  106. let globalOptions;
  107. let globalAction;
  108. let globalDefault;
  109. if (realTypeOf(paramOptions) === 'object') {
  110. function getValue(object, array) {
  111. const name = array.find(name => object.hasOwnProperty(name));
  112. if (typeof name !== 'undefined') {
  113. return object[name]
  114. }
  115. else {
  116. return undefined
  117. }
  118. }
  119. globalOptions = getValue(paramOptions, ['option', 'options', 'property', 'props', 'properties']);
  120. globalObject = getValue(paramOptions, ['object']);
  121. globalDefault = getValue(paramOptions, ['default']);
  122. globalAction = getValue(paramOptions, ['action', 'callback']);
  123. }
  124. else {
  125. globalOptions = paramOptions;
  126. globalDefault = paramDefault;
  127. globalAction = paramAction;
  128. }
  129. if (globalOptions === undefined) {
  130. return globalDefault
  131. }
  132. if (typeof globalAction === 'string' && globalAction === 'set') {
  133. return globalDefault
  134. }
  135. function doAction(option, action = undefined, object = {}) {
  136. if (typeof action === 'function') {
  137. return action(option, object)
  138. }
  139. else {
  140. return option
  141. }
  142. }
  143. if (realTypeOf(globalObject) === 'object') {
  144. if (realTypeOf(globalOptions) === 'array') {
  145. for (let i=0; i < globalOptions.length; i++) {
  146. if (globalObject.hasOwnProperty(globalOptions[i])) {
  147. const result = doAction(globalObject[globalOptions[i]], globalAction, globalObject);
  148. if (result !== undefined) {
  149. return result
  150. }
  151. }
  152. }
  153. }
  154. else if (typeof globalOptions === 'string' && globalObject.hasOwnProperty(globalOptions)) {
  155. const result = doAction(globalObject[globalOptions], globalAction, globalObject);
  156. if (result !== undefined) {
  157. return result
  158. }
  159. }
  160. }
  161. else if (realTypeOf(globalOptions) === 'array') {
  162. for (let i=0; i < globalOptions.length; i++) {
  163. if (globalOptions[i] !== undefined) {
  164. const result = doAction(globalOptions[i], globalAction, globalObject);
  165. if (result !== undefined) {
  166. return result
  167. }
  168. }
  169. }
  170. }
  171. else {
  172. const result = doAction(globalOptions, globalAction, globalObject);
  173. if (result !== undefined) {
  174. return result
  175. }
  176. }
  177. return globalDefault
  178. }
  179.  
  180. function qs({selector = null, scope = document, array = false, all = false, contains = null, unittest = false, debugTag = ''} = {}) {
  181. const language = {
  182. en: {
  183. selectorUndefined: `${debugTag}selector is undefined`,
  184. scopeNotFound: `${debugTag}scope not found`,
  185. }
  186. };
  187. if (unittest === 'language') {return language}
  188. try {
  189. if (selector === null) {
  190. console.error(language.en.selectorUndefined);
  191. return null
  192. }
  193. if (scope !== document) {
  194. scope = getNode(scope);
  195. if (scope === null) {
  196. return null
  197. }
  198. }
  199. if (unittest === 'scope') {return scope}
  200. if (unittest === 'options') {
  201. return {
  202. selector: selector,
  203. scope: scope,
  204. array: array,
  205. all: all,
  206. contains: contains,
  207. unittest: unittest
  208. }
  209. }
  210. if (all === true) {
  211. const staticNodeList = scope.querySelectorAll(selector);
  212. if (staticNodeList.length === 0) {return null}
  213. if (array === true) {
  214. if (contains !== null) {
  215. const tempArray = [];
  216. staticNodeList.forEach((element) => {
  217. if (element.textContent.search(contains) !== -1) {
  218. tempArray.push(element);
  219. }
  220. });
  221. if (tempArray.length === 0) {return null}
  222. else {return tempArray}
  223. }
  224. return Array.from(staticNodeList)
  225. }
  226. else {
  227. if (contains !== null) {
  228. for (let index = 0; index < staticNodeList.length; index++) {
  229. if (staticNodeList[index].textContent.search(contains) !== -1) {
  230. return staticNodeList
  231. }
  232. }
  233. return null
  234. }
  235. return staticNodeList
  236. }
  237. }
  238. else {
  239. const qsHTMLElement = scope.querySelector(selector);
  240. if (qsHTMLElement === null) {return null}
  241. if (typeof contains === 'string' || contains instanceof RegExp) {
  242. if (qsHTMLElement.textContent.search(contains) === -1) {return null}
  243. }
  244. if (array === true) {return [qsHTMLElement]}
  245. else {return qsHTMLElement}
  246. }
  247. }
  248. catch (error) {
  249. console.error(error);
  250. }
  251. }
  252.  
  253. function sprintf(...args) {
  254. if (Object.keys(args).length == 0) {
  255. return null
  256. }
  257. if (args[0] === '') {
  258. return null
  259. }
  260. const options = setDefault({
  261. options: [realTypeOf(args[0]) == 'object' ? args[0] : undefined],
  262. default: {regex: /{([^{}]+)}/g, template: args[0]},
  263. });
  264. if (realTypeOf(args[1]) == 'object') {
  265. return options.template.replace(options.regex, (match, n) => {
  266. for (let key = 1; args[key]; key++) {
  267. if (args[key][n]) {
  268. if (typeof args[key][n] == 'function') {
  269. return args[key][n]().toString()
  270. }
  271. return args[key][n]
  272. }
  273. }
  274. return match
  275. })
  276. }
  277. else {
  278. return options.template.replace(options.regex, (match, n) => {return args[n] || match})
  279. }
  280. }
  281.  
  282. const globals = {
  283. priceMatchRegExp: /((\d+[,\.])+\d+)/,
  284. currencySymbolsRegExp: /((((AU|C|US) )?\$)|EUR|PHP|zł|£)/,
  285. itemPriceElementTemplate: '<span class="total-price">{currencySymbol}{totalPrice}</span>'
  286. };
  287.  
  288. function processMethod(options) {
  289. try {
  290. function getMethod() {
  291. for (const [, method] of Object.entries(options)) {
  292. for (let index = 0; index < method.identifierSelector.length; index++) {
  293. const selector = method.identifierSelector[index];
  294. const identifierNode = getNode(selector);
  295. if (identifierNode !== null) {return method}
  296. }
  297. }
  298. return null
  299. }
  300. const method = getMethod(options);
  301. if (method !== null) {method.process();}
  302. }
  303. catch (error) {console.error(error);}
  304. }
  305.  
  306. function getValue(element) {
  307. try {
  308. let value = findMatch(element.textContent.trim(), globals.priceMatchRegExp);
  309. value = value.replace(/[,\.]/g, '');
  310. value = parseFloat(value);
  311. return value
  312. }
  313. catch (error) {
  314. console.error(error);
  315. return null
  316. }
  317. }
  318.  
  319. function processItemListing({listItemsSelector, itemPriceElementSelector, convertPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, convertShippingElementSelector, itemShippingElementTemplate = null}) {
  320. const content = qs({selector: listItemsSelector});
  321. if (content) {
  322. const itemPriceElement = qs({selector: convertPriceElementSelector, scope: content, contains: /\d/}) || qs({selector: itemPriceElementSelector, scope: content, contains: /\d/});
  323. const itemShippingElement = qs({selector: convertShippingElementSelector, scope: content, contains: /\d/}) || qs({selector: itemShippingElementSelector, scope: content, contains: /\d/});
  324. if (itemPriceElement && itemShippingElement) {
  325. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  326. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  327. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  328. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  329. const HTML = sprintf(
  330. itemShippingElementTemplate || itemPriceElementTemplate, {
  331. itemPrice: itemPriceElement.textContent.trim(),
  332. itemShippingAmount: itemShippingElement.textContent.trim(),
  333. currencySymbol: shippingCurrencySymbol,
  334. totalPrice: totalPrice});
  335. if (itemPriceElementTemplate) {
  336. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  337. }
  338. else {
  339. itemShippingElement.innerHTML = HTML;
  340. }
  341. }
  342. }
  343. }
  344. }
  345.  
  346. function processListGallery({listItemsSelector, itemPriceElementSelector, itemPriceElementTemplate = null, itemShippingElementSelector, itemShippingElementTemplate = null}) {
  347. const listItems = qs({selector: listItemsSelector, all: true, array: true});
  348. if (listItems) {
  349. for (let i=0; listItems[i]; i++) {
  350. const itemPriceElement = qs({selector: itemPriceElementSelector, scope: listItems[i]});
  351. const itemShippingElement = qs({selector: itemShippingElementSelector, scope: listItems[i], contains: /\d/});
  352. if (itemPriceElement && itemShippingElement) {
  353. const priceCurrencySymbol = findMatch(itemPriceElement.textContent.trim(), globals.currencySymbolsRegExp);
  354. const shippingCurrencySymbol = findMatch(itemShippingElement.textContent.trim(), globals.currencySymbolsRegExp);
  355. if (shippingCurrencySymbol && (shippingCurrencySymbol === priceCurrencySymbol)) {
  356. const totalPrice = ((getValue(itemPriceElement) + getValue(itemShippingElement)) / 100).toFixed(2);
  357. const HTML = sprintf(
  358. itemShippingElementTemplate || itemPriceElementTemplate, {
  359. itemPrice: itemPriceElement.textContent.trim(),
  360. itemShippingAmount: itemShippingElement.textContent.trim(),
  361. currencySymbol: shippingCurrencySymbol,
  362. totalPrice: totalPrice});
  363. if (itemPriceElementTemplate) {
  364. itemPriceElement.insertAdjacentHTML('afterend', HTML);
  365. }
  366. else {
  367. itemShippingElement.innerHTML = HTML;
  368. }
  369. }
  370. }
  371. }
  372. }
  373. }
  374.  
  375. var stylesheet = ".total-price{background:#bf0;color:#111!important;outline:2px solid;padding:1px 4px;margin-left:5px;font-size:20px!important;font-weight:400!important;}.s-item__detail{overflow:visible!important;}";
  376.  
  377. appendStyle({style: stylesheet});
  378. processMethod({
  379. search: {
  380. identifierSelector: ['#mainContent ul.srp-results', '#mainContent ul.b-list__items_nofooter'],
  381. process: () => processListGallery({
  382. listItemsSelector: '#mainContent li.s-item',
  383. itemPriceElementSelector: '.s-item__price',
  384. itemShippingElementSelector: '.s-item__shipping',
  385. itemPriceElementTemplate: globals.itemPriceElementTemplate
  386. })
  387. },
  388. sch: {
  389. identifierSelector: ['#mainContent ul#ListViewInner'],
  390. process: () => processListGallery({
  391. listItemsSelector: '#mainContent li',
  392. itemPriceElementSelector: '.lvprice span',
  393. itemShippingElementSelector: '.lvshipping span.fee',
  394. itemPriceElementTemplate: globals.itemPriceElementTemplate
  395. })
  396. },
  397. itm: {
  398. identifierSelector: ['#mainContent form[name="viactiondetails"]'],
  399. process: () => processItemListing({
  400. listItemsSelector: '#mainContent',
  401. itemPriceElementSelector: 'span[itemprop="price"]',
  402. convertPriceElementSelector: '#prcIsumConv',
  403. itemShippingElementSelector: '#fshippingCost',
  404. convertShippingElementSelector: '#convetedPriceId',
  405. itemPriceElementTemplate: globals.itemPriceElementTemplate
  406. })
  407. }
  408. });